From 8b410e83f958206a52117f141bc7a4144bc06a4d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 14:53:15 +0100 Subject: [PATCH] add a managed native menu exporter for win32 tray icon menus. --- src/Avalonia.Controls/ApiCompatBaseline.txt | 3 +- src/Avalonia.Controls/NativeMenu.Export.cs | 13 ++--- .../Platform/ITopLevelNativeMenuExporter.cs | 17 +++++- .../Platform/ITrayIconImpl.cs | 3 + src/Avalonia.Controls/TrayIcon.cs | 4 +- .../Remote/TrayIconStub.cs | 9 +++ src/Windows/Avalonia.Win32/TrayIconImpl.cs | 17 +++--- .../Win32NativeToManagedMenuExporter.cs | 58 +++++++++++++++++++ 8 files changed, 103 insertions(+), 21 deletions(-) create mode 100644 src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 4046900a3a..dd41c30e85 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -37,6 +37,7 @@ MembersMustExist : Member 'public System.Action Avalonia.Controls MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.Platform.ITopLevelNativeMenuExporter.SetNativeMenu(Avalonia.Controls.NativeMenu)' is present in the contract but not in the implementation. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. @@ -56,4 +57,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract. -Total Issues: 57 +Total Issues: 58 diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs index 0349df842b..6bfe5ebc82 100644 --- a/src/Avalonia.Controls/NativeMenu.Export.cs +++ b/src/Avalonia.Controls/NativeMenu.Export.cs @@ -52,15 +52,10 @@ namespace Avalonia.Controls } public static readonly AttachedProperty MenuProperty - = AvaloniaProperty.RegisterAttached("Menu"/*, validate: - (o, v) => - { - if(!(o is Application || o is TopLevel)) - throw new InvalidOperationException("NativeMenu.Menu property isn't valid on "+o.GetType()); - return v; - }*/); + = AvaloniaProperty.RegisterAttached("Menu"); public static void SetMenu(AvaloniaObject o, NativeMenu menu) => o.SetValue(MenuProperty, menu); + public static NativeMenu GetMenu(AvaloniaObject o) => o.GetValue(MenuProperty); static NativeMenu() @@ -79,6 +74,10 @@ namespace Avalonia.Controls { GetInfo(tl).Exporter?.SetNativeMenu(args.NewValue.GetValueOrDefault()); } + else if(args.Sender is INativeMenuExporterProvider provider) + { + provider.NativeMenuExporter?.SetNativeMenu(args.NewValue.GetValueOrDefault()); + } }); } } diff --git a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs index 3ac5f28956..5e5f7b18ec 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs @@ -1,14 +1,25 @@ using System; -using System.Collections.Generic; using Avalonia.Platform; +#nullable enable + namespace Avalonia.Controls.Platform { - public interface ITopLevelNativeMenuExporter + public interface INativeMenuExporter + { + void SetNativeMenu(NativeMenu menu); + } + + public interface ITopLevelNativeMenuExporter : INativeMenuExporter { bool IsNativeMenuExported { get; } + event EventHandler OnIsNativeMenuExportedChanged; - void SetNativeMenu(NativeMenu menu); + } + + public interface INativeMenuExporterProvider + { + INativeMenuExporter? NativeMenuExporter { get; } } public interface ITopLevelImplWithNativeMenuExporter : ITopLevelImpl diff --git a/src/Avalonia.Controls/Platform/ITrayIconImpl.cs b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs index 9efa53ad16..e1eb62821b 100644 --- a/src/Avalonia.Controls/Platform/ITrayIconImpl.cs +++ b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Controls; +using Avalonia.Controls.Platform; #nullable enable @@ -21,5 +22,7 @@ namespace Avalonia.Platform /// Sets if the tray icon is visible or not. /// void SetIsVisible (bool visible); + + INativeMenuExporter? MenuExporter { get; } } } diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index f3a577f0b9..8f422dd4d4 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -12,7 +12,7 @@ namespace Avalonia.Controls { } - public class TrayIcon : AvaloniaObject, IDataContextProvider + public class TrayIcon : AvaloniaObject, IDataContextProvider, INativeMenuExporterProvider { private readonly ITrayIconImpl _impl; @@ -141,6 +141,8 @@ namespace Avalonia.Controls set => SetValue(IsVisibleProperty, value); } + public INativeMenuExporter? NativeMenuExporter => _impl.MenuExporter; + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); diff --git a/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs b/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs index 939cf16824..13d38660eb 100644 --- a/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs +++ b/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs @@ -1,4 +1,6 @@ using System; +using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Platform; namespace Avalonia.DesignerSupport.Remote @@ -9,6 +11,8 @@ namespace Avalonia.DesignerSupport.Remote public Action DoubleClicked { get; set; } public Action RightClicked { get; set; } + public INativeMenuExporter MenuExporter => throw new NotImplementedException(); + public void SetIcon(IWindowIconImpl icon) { } @@ -17,6 +21,11 @@ namespace Avalonia.DesignerSupport.Remote { } + public void SetMenu(NativeMenu menu) + { + throw new NotImplementedException(); + } + public void SetToolTipText(string text) { } diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index 2cbcd4a675..cdd0c3a2f6 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.LogicalTree; using Avalonia.Platform; @@ -21,9 +22,12 @@ namespace Avalonia.Win32 private bool _iconAdded; private IconImpl? _icon; private string? _tooltipText; + private readonly Win32NativeToManagedMenuExporter _exporter; private static Dictionary s_trayIcons = new Dictionary(); + public INativeMenuExporter MenuExporter => _exporter; + internal static void ProcWnd(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { if (msg == (int)CustomWindowsMessage.WM_TRAYMOUSE && s_trayIcons.ContainsKey(wParam.ToInt32())) @@ -34,6 +38,8 @@ namespace Avalonia.Win32 public TrayIconImpl() { + _exporter = new Win32NativeToManagedMenuExporter(); + _uniqueId = ++_nextUniqueId; s_trayIcons.Add(_uniqueId, this); @@ -125,7 +131,7 @@ namespace Avalonia.Win32 } } - private static void OnRightClicked() + private void OnRightClicked() { var _trayMenu = new TrayPopupRoot() { @@ -135,14 +141,7 @@ namespace Avalonia.Win32 TransparencyLevelHint = WindowTransparencyLevel.Transparent, Content = new TrayIconMenuFlyoutPresenter() { - Items = new List - { - new MenuItem { Header = "Item 1"}, - new MenuItem { Header = "Item 2"}, - new MenuItem { Header = "Item 3"}, - new MenuItem { Header = "Item 4"}, - new MenuItem { Header = "Item 5"} - } + Items = _exporter.GetMenu() } }; diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs new file mode 100644 index 0000000000..57fccad633 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Controls.Platform; + +#nullable enable + +namespace Avalonia.Win32 +{ + internal class Win32NativeToManagedMenuExporter : INativeMenuExporter + { + private NativeMenu? _nativeMenu; + + public void SetNativeMenu(NativeMenu nativeMenu) + { + _nativeMenu = nativeMenu; + } + + private IEnumerable? Populate (NativeMenu nativeMenu) + { + var items = new List(); + + foreach (var menuItem in nativeMenu.Items) + { + if (menuItem is NativeMenuItemSeparator separator) + { + items.Add(new MenuItem { Header = "-" }); + } + else if (menuItem is NativeMenuItem item) + { + var newItem = new MenuItem { Header = item.Header, Icon = item.Icon, Command = item.Command, CommandParameter = item.CommandParameter }; + + if(item.Menu != null) + { + newItem.ContextMenu = new ContextMenu() { Items = Populate(item.Menu) }; + } + else if (item.HasClickHandlers && item is INativeMenuItemExporterEventsImplBridge bridge) + { + newItem.Click += (_,_) => bridge.RaiseClicked(); + } + + items.Add(newItem); + } + } + + return items; + } + + public IEnumerable? GetMenu () + { + if(_nativeMenu != null) + { + return Populate(_nativeMenu); + } + + return null; + } + } +}