From 192eb4d1c035eb318ba3119c1d27d379cff9d818 Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Thu, 15 Jan 2026 00:53:14 -0800 Subject: [PATCH] Add context menu support for results in Avalonia UI - Add ActiveView enum to track Results vs ContextMenu view state - Add ContextMenu ResultsViewModel and view switching logic - Implement LoadContextMenuCommand using PluginManager.GetContextMenusForPlugin - Add keyboard navigation: Shift+Enter/Right to open, Left/Escape to close - Update SelectNextItem/SelectPrevItem to navigate appropriate list - Update EscCommand to return from context menu before hiding --- Flow.Launcher.Avalonia/MainWindow.axaml | 15 +- Flow.Launcher.Avalonia/MainWindow.axaml.cs | 31 +++- .../ViewModel/MainViewModel.cs | 140 +++++++++++++++++- 3 files changed, 176 insertions(+), 10 deletions(-) diff --git a/Flow.Launcher.Avalonia/MainWindow.axaml b/Flow.Launcher.Avalonia/MainWindow.axaml index 42e18db4d..5e603fe44 100644 --- a/Flow.Launcher.Avalonia/MainWindow.axaml +++ b/Flow.Launcher.Avalonia/MainWindow.axaml @@ -34,6 +34,7 @@ + @@ -78,7 +79,7 @@ + IsVisible="{Binding ShowResultsArea}" /> @@ -89,12 +90,20 @@ - - + + + + + + + = (_viewModel.QueryText?.Length ?? 0)) + { + _viewModel.LoadContextMenuCommand.Execute(null); + e.Handled = true; + return; + } + } + + // Handle Left Arrow to go back from context menu + if (e.Key == Key.Left && _viewModel != null && _viewModel.IsContextMenuViewActive) + { + _viewModel.BackToResultsCommand.Execute(null); + e.Handled = true; + return; } } diff --git a/Flow.Launcher.Avalonia/ViewModel/MainViewModel.cs b/Flow.Launcher.Avalonia/ViewModel/MainViewModel.cs index a88f4b41e..be9c86170 100644 --- a/Flow.Launcher.Avalonia/ViewModel/MainViewModel.cs +++ b/Flow.Launcher.Avalonia/ViewModel/MainViewModel.cs @@ -13,6 +13,15 @@ using Flow.Launcher.Plugin; namespace Flow.Launcher.Avalonia.ViewModel; +/// +/// Represents which view is currently active. +/// +public enum ActiveView +{ + Results, + ContextMenu +} + /// /// MainViewModel for Avalonia - minimal implementation for plugin queries. /// @@ -41,12 +50,46 @@ public partial class MainViewModel : ObservableObject [ObservableProperty] private ResultsViewModel _results; + [ObservableProperty] + private ResultsViewModel _contextMenu; + + [ObservableProperty] + private ActiveView _activeView = ActiveView.Results; + + /// + /// Whether the results view is currently active. + /// + public bool IsResultsViewActive => ActiveView == ActiveView.Results; + + /// + /// Whether the context menu view is currently active. + /// + public bool IsContextMenuViewActive => ActiveView == ActiveView.ContextMenu; + + /// + /// Whether to show the results/context menu area (separator + list). + /// + public bool ShowResultsArea => HasResults || IsContextMenuViewActive; + public Settings Settings => _settings; public MainViewModel(Settings settings) { _settings = settings; _results = new ResultsViewModel(settings); + _contextMenu = new ResultsViewModel(settings); + } + + partial void OnActiveViewChanged(ActiveView value) + { + OnPropertyChanged(nameof(IsResultsViewActive)); + OnPropertyChanged(nameof(IsContextMenuViewActive)); + OnPropertyChanged(nameof(ShowResultsArea)); + } + + partial void OnHasResultsChanged(bool value) + { + OnPropertyChanged(nameof(ShowResultsArea)); } public void OnPluginsReady() @@ -92,10 +135,22 @@ public partial class MainViewModel : ObservableObject { MainWindowVisibility = false; QueryText = ""; + ActiveView = ActiveView.Results; + ContextMenu.Clear(); HideRequested?.Invoke(); Log.Info(ClassName, "Hide requested"); } + /// + /// Go back from context menu to results view. + /// + [RelayCommand] + private void BackToResults() + { + ActiveView = ActiveView.Results; + ContextMenu.Clear(); + } + partial void OnQueryTextChanged(string value) => _ = QueryAsync(); private async Task QueryAsync() @@ -220,21 +275,96 @@ public partial class MainViewModel : ObservableObject } [RelayCommand] - private void Esc() { Hide(); } + private void Esc() + { + // If in context menu, go back to results; otherwise hide window + if (ActiveView == ActiveView.ContextMenu) + { + BackToResults(); + } + else + { + Hide(); + } + } [RelayCommand] private async Task OpenResultAsync() { - var result = Results.SelectedItem?.PluginResult; + Result? result; + if (ActiveView == ActiveView.ContextMenu) + { + result = ContextMenu.SelectedItem?.PluginResult; + } + else + { + result = Results.SelectedItem?.PluginResult; + } + if (result == null) return; + try { if (await result.ExecuteAsync(new ActionContext { SpecialKeyState = SpecialKeyState.Default })) - HideRequested?.Invoke(); + { + Hide(); + } + else if (ActiveView == ActiveView.ContextMenu) + { + // If context menu action didn't hide, go back to results + BackToResults(); + } } catch (Exception e) { Log.Exception(ClassName, "Execute error", e); } } - [RelayCommand] private void SelectNextItem() => Results.SelectNextItem(); - [RelayCommand] private void SelectPrevItem() => Results.SelectPrevItem(); + /// + /// Load context menu for the currently selected result. + /// + [RelayCommand] + private void LoadContextMenu() + { + var selectedResult = Results.SelectedItem?.PluginResult; + if (selectedResult == null) return; + + try + { + var contextMenuResults = PluginManager.GetContextMenusForPlugin(selectedResult); + if (contextMenuResults == null || contextMenuResults.Count == 0) return; + + var contextMenuItems = contextMenuResults.Select(r => new ResultViewModel + { + Title = r.Title ?? "", + SubTitle = r.SubTitle ?? "", + IconPath = r.IcoPath ?? "", + Score = r.Score, + PluginResult = r + }).ToList(); + + ContextMenu.ReplaceResults(contextMenuItems); + ActiveView = ActiveView.ContextMenu; + } + catch (Exception e) + { + Log.Exception(ClassName, "Failed to load context menu", e); + } + } + + [RelayCommand] + private void SelectNextItem() + { + if (ActiveView == ActiveView.ContextMenu) + ContextMenu.SelectNextItem(); + else + Results.SelectNextItem(); + } + + [RelayCommand] + private void SelectPrevItem() + { + if (ActiveView == ActiveView.ContextMenu) + ContextMenu.SelectPrevItem(); + else + Results.SelectPrevItem(); + } }