Add routing menu to Desktop tray icon

Add routing selection functionality to the system tray menu for
macOS/Linux (Avalonia Desktop), bringing feature parity with the
Windows version which already has this capability.

- Add dynamic routing items to NativeMenu in App.axaml
- Implement menu item click handling and state refresh in App.axaml.cs
- Support radio-style selection for routing options

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
叶明江 2026-03-23 23:29:20 +08:00
parent 661affd6a5
commit 01a5805d98
2 changed files with 123 additions and 0 deletions

View file

@ -64,6 +64,14 @@
IsVisible="{Binding BlSystemProxyPacVisible}"
ToggleType="Radio" />
<NativeMenuItemSeparator />
<NativeMenuItem
Header="{x:Static resx:ResUI.menuRouting}"
IsVisible="{Binding BlRouting}">
<NativeMenuItem.Menu>
<NativeMenu />
</NativeMenuItem.Menu>
</NativeMenuItem>
<NativeMenuItemSeparator />
<NativeMenuItem Click="MenuAddServerViaClipboardClick" Header="{x:Static resx:ResUI.menuAddServerViaClipboard}" />
<NativeMenuItem Command="{Binding SubUpdateCmd}" Header="{x:Static resx:ResUI.menuSubUpdate}" />
<NativeMenuItem Command="{Binding SubUpdateViaProxyCmd}" Header="{x:Static resx:ResUI.menuSubUpdateViaProxy}" />

View file

@ -4,6 +4,8 @@ namespace v2rayN.Desktop;
public partial class App : Application
{
private readonly Dictionary<NativeMenuItem, string> _routingMenuMap = [];
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
@ -20,6 +22,7 @@ public partial class App : Application
{
AppManager.Instance.InitComponents();
DataContext = StatusBarViewModel.Instance;
InitRoutingMenu();
}
desktop.Exit += OnExit;
@ -44,6 +47,118 @@ public partial class App : Application
private void OnExit(object? sender, ControlledApplicationLifetimeExitEventArgs e)
{
foreach (var menuItem in _routingMenuMap.Keys)
{
menuItem.Click -= MenuRoutingClick;
}
_routingMenuMap.Clear();
}
private void InitRoutingMenu()
{
var vm = StatusBarViewModel.Instance;
vm.RoutingItems.CollectionChanged += (_, _) => RefreshRoutingMenuItems();
vm.PropertyChanged += (_, args) =>
{
if (args.PropertyName == nameof(StatusBarViewModel.SelectedRouting))
{
RefreshRoutingMenuCheckState();
}
};
RefreshRoutingMenuItems();
}
private void RefreshRoutingMenuItems()
{
Dispatcher.UIThread.Post(() =>
{
var routingMenu = GetRoutingRootMenu();
if (routingMenu == null)
{
return;
}
routingMenu.Menu ??= new NativeMenu();
foreach (var menuItem in _routingMenuMap.Keys)
{
menuItem.Click -= MenuRoutingClick;
}
_routingMenuMap.Clear();
routingMenu.Menu.Items.Clear();
foreach (var routing in StatusBarViewModel.Instance.RoutingItems)
{
var menuItem = new NativeMenuItem
{
Header = routing.Remarks,
IsChecked = routing.IsActive,
ToggleType = NativeMenuItemToggleType.Radio,
};
menuItem.Click += MenuRoutingClick;
_routingMenuMap[menuItem] = routing.Id;
routingMenu.Menu.Items.Add(menuItem);
}
RefreshRoutingMenuCheckState();
}, DispatcherPriority.Background);
}
private void RefreshRoutingMenuCheckState()
{
Dispatcher.UIThread.Post(() =>
{
var selectedRoutingId = StatusBarViewModel.Instance.SelectedRouting?.Id;
foreach (var pair in _routingMenuMap)
{
pair.Key.IsChecked = pair.Value == selectedRoutingId;
}
}, DispatcherPriority.Background);
}
private NativeMenuItem? GetRoutingRootMenu()
{
if (Current == null)
{
return null;
}
var icons = TrayIcon.GetIcons(Current);
if (icons.Count == 0 || icons[0].Menu is not NativeMenu trayMenu)
{
return null;
}
return trayMenu.Items
.OfType<NativeMenuItem>()
.FirstOrDefault(x => Equals(x.Header, ResUI.menuRouting));
}
private void MenuRoutingClick(object? sender, EventArgs e)
{
if (sender is not NativeMenuItem menuItem)
{
return;
}
if (!_routingMenuMap.TryGetValue(menuItem, out var routingId))
{
return;
}
var target = StatusBarViewModel.Instance.RoutingItems.FirstOrDefault(x => x.Id == routingId);
if (target == null)
{
return;
}
if (StatusBarViewModel.Instance.SelectedRouting?.Id == target.Id)
{
RefreshRoutingMenuCheckState();
return;
}
StatusBarViewModel.Instance.SelectedRouting = target;
}
private async void MenuAddServerViaClipboardClick(object? sender, EventArgs e)