mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
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
This commit is contained in:
parent
2466b907c4
commit
192eb4d1c0
3 changed files with 176 additions and 10 deletions
|
|
@ -34,6 +34,7 @@
|
|||
<KeyBinding Gesture="Enter" Command="{Binding OpenResultCommand}" />
|
||||
<KeyBinding Gesture="Down" Command="{Binding SelectNextItemCommand}" />
|
||||
<KeyBinding Gesture="Up" Command="{Binding SelectPrevItemCommand}" />
|
||||
<KeyBinding Gesture="Shift+Enter" Command="{Binding LoadContextMenuCommand}" />
|
||||
</Window.KeyBindings>
|
||||
|
||||
<!-- Main container with rounded corners and background -->
|
||||
|
|
@ -78,7 +79,7 @@
|
|||
<!-- Separator between query and results -->
|
||||
<Rectangle Name="MiddleSeparator"
|
||||
Classes="separator"
|
||||
IsVisible="{Binding HasResults}" />
|
||||
IsVisible="{Binding ShowResultsArea}" />
|
||||
|
||||
<!-- Results Area -->
|
||||
<Border Name="ResultAreaBorder" Classes="resultAreaBorder">
|
||||
|
|
@ -89,12 +90,20 @@
|
|||
<ColumnDefinition Width="0" /> <!-- Preview panel, hidden for now -->
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Result List -->
|
||||
<StackPanel Name="ResultArea" Grid.Column="0">
|
||||
<!-- Result List (visible when in Results view) -->
|
||||
<StackPanel Name="ResultArea" Grid.Column="0"
|
||||
IsVisible="{Binding IsResultsViewActive}">
|
||||
<views:ResultListBox Name="ResultListBox"
|
||||
DataContext="{Binding Results}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Context Menu List (visible when in ContextMenu view) -->
|
||||
<StackPanel Name="ContextMenuArea" Grid.Column="0"
|
||||
IsVisible="{Binding IsContextMenuViewActive}">
|
||||
<views:ResultListBox Name="ContextMenuListBox"
|
||||
DataContext="{Binding ContextMenu}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Preview Panel Separator (hidden for now) -->
|
||||
<GridSplitter Name="PreviewSplitter"
|
||||
Grid.Column="1"
|
||||
|
|
|
|||
|
|
@ -80,11 +80,38 @@ public partial class MainWindow : Window
|
|||
{
|
||||
base.OnKeyDown(e);
|
||||
|
||||
// Handle Escape to hide window
|
||||
// Handle Escape to hide window (handled by command, but keep as fallback)
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
Hide();
|
||||
_viewModel?.EscCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Right Arrow to open context menu when cursor is at end of query
|
||||
if (e.Key == Key.Right && _viewModel != null)
|
||||
{
|
||||
// Only trigger context menu if:
|
||||
// 1. We're in results view
|
||||
// 2. There's a selected result
|
||||
// 3. Cursor is at the end of the query text
|
||||
if (_viewModel.IsResultsViewActive &&
|
||||
_viewModel.Results.SelectedItem != null &&
|
||||
_queryTextBox != null &&
|
||||
_queryTextBox.CaretIndex >= (_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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,15 @@ using Flow.Launcher.Plugin;
|
|||
|
||||
namespace Flow.Launcher.Avalonia.ViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// Represents which view is currently active.
|
||||
/// </summary>
|
||||
public enum ActiveView
|
||||
{
|
||||
Results,
|
||||
ContextMenu
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MainViewModel for Avalonia - minimal implementation for plugin queries.
|
||||
/// </summary>
|
||||
|
|
@ -41,12 +50,46 @@ public partial class MainViewModel : ObservableObject
|
|||
[ObservableProperty]
|
||||
private ResultsViewModel _results;
|
||||
|
||||
[ObservableProperty]
|
||||
private ResultsViewModel _contextMenu;
|
||||
|
||||
[ObservableProperty]
|
||||
private ActiveView _activeView = ActiveView.Results;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the results view is currently active.
|
||||
/// </summary>
|
||||
public bool IsResultsViewActive => ActiveView == ActiveView.Results;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the context menu view is currently active.
|
||||
/// </summary>
|
||||
public bool IsContextMenuViewActive => ActiveView == ActiveView.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to show the results/context menu area (separator + list).
|
||||
/// </summary>
|
||||
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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go back from context menu to results view.
|
||||
/// </summary>
|
||||
[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();
|
||||
/// <summary>
|
||||
/// Load context menu for the currently selected result.
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue