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();
+ }
}