Skip to content

Commit 9b99cf0

Browse files
committed
feat: enhance command input gestures and menu item shortcuts
1 parent 19e826d commit 9b99cf0

File tree

3 files changed

+89
-7
lines changed

3 files changed

+89
-7
lines changed

src/ProjectRover.Shims/SystemWindowsInputShim.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Avalonia.Input;
55
using System.Linq;
66
using System.Collections.Generic;
7+
using ModifierKeys = Avalonia.Input.KeyModifiers;
78

89
namespace System.Windows.Input
910
{
@@ -74,11 +75,21 @@ public void Dispose()
7475

7576
public static class NavigationCommands
7677
{
77-
public static readonly RoutedCommand BrowseBack = new RoutedCommand();
78-
public static readonly RoutedCommand BrowseForward = new RoutedCommand();
78+
public static readonly RoutedCommand BrowseBack = Create("BrowseBack", new KeyGesture(Key.Left, ModifierKeys.Alt));
79+
public static readonly RoutedCommand BrowseForward = Create("BrowseForward", new KeyGesture(Key.Right, ModifierKeys.Alt));
7980

80-
public static readonly RoutedCommand Refresh = new RoutedCommand();
81-
public static readonly RoutedCommand Search = new RoutedCommand();
81+
public static readonly RoutedCommand Refresh = Create("Refresh", new KeyGesture(Key.F5));
82+
public static readonly RoutedCommand Search = Create("Search", null); // ShowSearchCommand overrides gestures
83+
84+
static RoutedCommand Create(string name, KeyGesture? defaultGesture)
85+
{
86+
var cmd = new RoutedCommand(name);
87+
if (defaultGesture != null)
88+
{
89+
cmd.InputGestures.Add(defaultGesture);
90+
}
91+
return cmd;
92+
}
8293
}
8394

8495
// Very small RoutedCommand implementation for shimming purposes

src/ProjectRover.Shims/SystemWindowsShim.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
using Avalonia.Layout;
88
using Avalonia.Media;
99
using Avalonia.Threading;
10+
using Avalonia.Input;
11+
using ModifierKeys = Avalonia.Input.KeyModifiers;
1012
using System.Windows.Input;
1113

1214
namespace System.Windows
@@ -49,9 +51,19 @@ public static string GetText()
4951
public static class ApplicationCommands
5052
{
5153
// Provide commonly used RoutedUICommands as placeholders
52-
public static readonly RoutedUICommand Save = new("Save", "Save", typeof(ApplicationCommands));
53-
public static readonly RoutedUICommand Open = new("Open", "Open", typeof(ApplicationCommands));
54-
public static readonly RoutedUICommand Close = new("Close", "Close", typeof(ApplicationCommands));
54+
public static readonly RoutedUICommand Save = Create("Save", "Save", new KeyGesture(Key.S, ModifierKeys.Control));
55+
public static readonly RoutedUICommand Open = Create("Open", "Open", new KeyGesture(Key.O, ModifierKeys.Control));
56+
public static readonly RoutedUICommand Close = Create("Close", "Close", new KeyGesture(Key.F4, ModifierKeys.Control));
57+
58+
static RoutedUICommand Create(string text, string name, KeyGesture defaultGesture)
59+
{
60+
var cmd = new RoutedUICommand(text, name, typeof(ApplicationCommands));
61+
if (defaultGesture != null)
62+
{
63+
cmd.InputGestures.Add(defaultGesture);
64+
}
65+
return cmd;
66+
}
5567
}
5668

5769
// Minimal mock for DelegateCommand to allow ILSpy code to compile.

src/ProjectRover/Controls/MainMenu.axaml.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ void InitMainMenu(Menu mainMenu, IExportProvider exportProvider)
133133
Tag = entry.Metadata?.MenuID,
134134
Header = headerText ?? entry.Metadata?.Header
135135
};
136+
ApplyGesture(menuItem, entry.Metadata?.InputGestureText, cmd);
136137
if (!string.IsNullOrEmpty(entry.Metadata?.MenuIcon))
137138
{
138139
try
@@ -556,6 +557,11 @@ static MenuItem CreateToolPaneMenuItem(ICSharpCode.ILSpy.ViewModels.ToolPaneMode
556557
Tag = toolPane.ContentId,
557558
Command = toolPane.AssociatedCommand ?? new ICSharpCode.ILSpy.Commands.ToolPaneCommand(toolPane.ContentId, dockWorkspace)
558559
};
560+
if (toolPane.ShortcutKey != null)
561+
{
562+
menuItem.HotKey = toolPane.ShortcutKey;
563+
menuItem.InputGesture = toolPane.ShortcutKey;
564+
}
559565

560566
// Add icon if available
561567
if (!string.IsNullOrEmpty(toolPane.Icon))
@@ -920,6 +926,41 @@ void UpdateIcon()
920926
}
921927
}
922928

929+
static void ApplyGesture(MenuItem menuItem, string? gestureText, ICommand cmd)
930+
{
931+
// Prefer explicit metadata text; otherwise fall back to routed command gestures.
932+
var resolvedCommand = ICSharpCode.ILSpy.CommandWrapper.Unwrap(cmd);
933+
if (string.IsNullOrWhiteSpace(gestureText) && resolvedCommand is System.Windows.Input.RoutedCommand routed)
934+
{
935+
var routedGesture = routed.InputGestures?.OfType<KeyGesture>().FirstOrDefault();
936+
gestureText = routedGesture?.ToString();
937+
}
938+
else if (string.IsNullOrWhiteSpace(gestureText) && resolvedCommand is System.Windows.Input.RoutedUICommand routedUi)
939+
{
940+
var routedGesture = routedUi.InputGestures?.OfType<KeyGesture>().FirstOrDefault();
941+
gestureText = routedGesture?.ToString();
942+
}
943+
944+
if (string.IsNullOrWhiteSpace(gestureText))
945+
return;
946+
947+
try
948+
{
949+
// HotKey drives visual hint rendering in Avalonia menus
950+
var parsed = KeyGesture.Parse(gestureText);
951+
menuItem.HotKey = parsed;
952+
menuItem.InputGesture = parsed; // ensures hint is rendered even if HotKey isn't
953+
}
954+
catch
955+
{
956+
// Fallback: append gesture text so it is still visible
957+
if (menuItem.Header is string headerText)
958+
{
959+
menuItem.Header = $"{headerText}\t{gestureText}";
960+
}
961+
}
962+
}
963+
923964
void BuildNativeMenu(Menu mainMenu)
924965
{
925966
if (!OperatingSystem.IsMacOS())
@@ -1056,6 +1097,24 @@ NativeMenuItem Convert(MenuItem m)
10561097

10571098
log.Debug("BuildNativeMenu: Converted {ItemCount} menu items total", itemCount);
10581099

1100+
// Insert an explicit application menu to avoid the platform/Avalonia
1101+
// falling back to a default menu (which can show "About Avalonia").
1102+
try
1103+
{
1104+
var appName = Application.Current?.Name
1105+
?? Assembly.GetEntryAssembly()?.GetName().Name
1106+
?? "Project Rover";
1107+
var appSub = new NativeMenu();
1108+
// Optionally populate appSub with About/Preferences/Quit entries.
1109+
var appMenuItem = new NativeMenuItem { Header = appName, Menu = appSub };
1110+
nativeRoot.Items.Insert(0, appMenuItem);
1111+
log.Debug("BuildNativeMenu: Inserted application menu '{AppName}' to override default app menu", appName);
1112+
}
1113+
catch (Exception ex)
1114+
{
1115+
log.Warning(ex, "BuildNativeMenu: Failed to insert application menu");
1116+
}
1117+
10591118
// Always set a fresh native menu; avoid reusing existing items to prevent parent conflicts.
10601119
// Some native backends may throw when attempting to update menus that are still
10611120
// referenced by the platform. Always clear existing menu first before setting new one.

0 commit comments

Comments
 (0)