From 01a5805d98eb34f26641921562cff6d517909f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B6=E6=98=8E=E6=B1=9F?= Date: Mon, 23 Mar 2026 23:29:20 +0800 Subject: [PATCH] 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 Co-Authored-By: Happy --- v2rayN/v2rayN.Desktop/App.axaml | 8 ++ v2rayN/v2rayN.Desktop/App.axaml.cs | 115 +++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/v2rayN/v2rayN.Desktop/App.axaml b/v2rayN/v2rayN.Desktop/App.axaml index 21204f60..ba9ef347 100644 --- a/v2rayN/v2rayN.Desktop/App.axaml +++ b/v2rayN/v2rayN.Desktop/App.axaml @@ -64,6 +64,14 @@ IsVisible="{Binding BlSystemProxyPacVisible}" ToggleType="Radio" /> + + + + + + diff --git a/v2rayN/v2rayN.Desktop/App.axaml.cs b/v2rayN/v2rayN.Desktop/App.axaml.cs index 35371f0f..25c51b6b 100644 --- a/v2rayN/v2rayN.Desktop/App.axaml.cs +++ b/v2rayN/v2rayN.Desktop/App.axaml.cs @@ -4,6 +4,8 @@ namespace v2rayN.Desktop; public partial class App : Application { + private readonly Dictionary _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() + .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)