From 634cb5e77846c388abf57aa969a6dc1206edd0bf Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sat, 11 Dec 2021 03:32:16 +0300 Subject: [PATCH] [OSX] Refactor Native App Menu. Move default menu initialization code from ObjC to C#. (#6909) * [NativeMenu] [Refactoring] Move Default Menu creation from native lib to C# (C# side of code) * fix return type for IAvnApplicationCommands * [Native] menu refactoring (ObjC side) * fix nullref * minor refactor Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com> Co-authored-by: Dan Walmsley --- native/Avalonia.Native/src/OSX/app.mm | 27 +++++++ native/Avalonia.Native/src/OSX/common.h | 15 +++- native/Avalonia.Native/src/OSX/main.mm | 44 +++++------ native/Avalonia.Native/src/OSX/menu.mm | 53 ++------------ .../Platform/INativeApplicationCommands.cs | 12 +++ .../AvaloniaNativeMenuExporter.cs | 73 +++++++++++++++++-- src/Avalonia.Native/AvaloniaNativePlatform.cs | 5 +- src/Avalonia.Native/IAvnMenuItem.cs | 5 ++ .../MacOSNativeMenuCommands.cs | 35 +++++++++ src/Avalonia.Native/avn.idl | 11 ++- 10 files changed, 198 insertions(+), 82 deletions(-) create mode 100644 src/Avalonia.Controls/Platform/INativeApplicationCommands.cs create mode 100644 src/Avalonia.Native/MacOSNativeMenuCommands.cs diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index e1972b22f4..79175d9ff1 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -97,3 +97,30 @@ extern void InitializeAvnApp(IAvnApplicationEvents* events) id delegate = [[AvnAppDelegate alloc] initWithEvents:events]; [app setDelegate:delegate]; } + +HRESULT AvnApplicationCommands::HideApp() +{ + START_COM_CALL; + [[NSApplication sharedApplication] hide:[NSApp delegate]]; + return S_OK; +} + +HRESULT AvnApplicationCommands::ShowAll() +{ + START_COM_CALL; + [[NSApplication sharedApplication] unhideAllApplications:[NSApp delegate]]; + return S_OK; +} + +HRESULT AvnApplicationCommands::HideOthers() +{ + START_COM_CALL; + [[NSApplication sharedApplication] hideOtherApplications:[NSApp delegate]]; + return S_OK; +} + + +extern IAvnApplicationCommands* CreateApplicationCommands() +{ + return new AvnApplicationCommands(); +} diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 8896fbe88b..126c9aa87b 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -25,12 +25,12 @@ extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events); extern IAvnTrayIcon* CreateTrayIcon(); extern IAvnMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItemSeparator(); +extern IAvnApplicationCommands* CreateApplicationCommands(); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu); +extern void SetServicesMenu (IAvnMenu* menu); extern IAvnMenu* GetAppMenu (); extern NSMenuItem* GetAppMenuItem (); -extern void SetAutoGenerateDefaultAppMenuItems (bool enabled); -extern bool GetAutoGenerateDefaultAppMenuItems (); extern void InitializeAvnApp(IAvnApplicationEvents* events); extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; @@ -67,4 +67,15 @@ public: ~AvnInsidePotentialDeadlock(); }; + +class AvnApplicationCommands : public ComSingleObject +{ +public: + FORWARD_IUNKNOWN() + + virtual HRESULT HideApp() override; + virtual HRESULT ShowAll() override; + virtual HRESULT HideOthers() override; +}; + #endif diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index eeaaecfdbd..69f2995847 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -2,7 +2,6 @@ #define COM_GUIDS_MATERIALIZE #include "common.h" -static bool s_generateDefaultAppMenuItems = true; static NSString* s_appTitle = @"Avalonia"; // Copyright (c) 2011 The Chromium Authors. All rights reserved. @@ -134,16 +133,6 @@ public: } } - virtual HRESULT SetDisableDefaultApplicationMenuItems (bool enabled) override - { - START_COM_CALL; - - @autoreleasepool - { - SetAutoGenerateDefaultAppMenuItems(!enabled); - return S_OK; - } - } }; /// See "Using POSIX Threads in a Cocoa Application" section here: @@ -357,6 +346,29 @@ public: return S_OK; } } + + virtual HRESULT SetServicesMenu (IAvnMenu* servicesMenu) override + { + START_COM_CALL; + + @autoreleasepool + { + ::SetServicesMenu(servicesMenu); + return S_OK; + } + } + + virtual HRESULT CreateApplicationCommands (IAvnApplicationCommands** ppv) override + { + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateApplicationCommands(); + return S_OK; + } + } + }; extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative() @@ -410,13 +422,3 @@ CGFloat PrimaryDisplayHeight() { return NSMaxY([[[NSScreen screens] firstObject] frame]); } - -void SetAutoGenerateDefaultAppMenuItems (bool enabled) -{ - s_generateDefaultAppMenuItems = enabled; -} - -bool GetAutoGenerateDefaultAppMenuItems () -{ - return s_generateDefaultAppMenuItems; -} diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 38f8c2a7cb..2dbe76bc6d 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -490,53 +490,6 @@ extern void SetAppMenu (NSString* appName, IAvnMenu* menu) { [s_appMenuItem setSubmenu:[NSMenu new]]; } - - auto appMenu = [s_appMenuItem submenu]; - - if(GetAutoGenerateDefaultAppMenuItems()) - { - [appMenu addItem:[NSMenuItem separatorItem]]; - - // Services item and menu - auto servicesItem = [[NSMenuItem alloc] init]; - servicesItem.title = @"Services"; - NSMenu *servicesMenu = [[NSMenu alloc] initWithTitle:@"Services"]; - servicesItem.submenu = servicesMenu; - [NSApplication sharedApplication].servicesMenu = servicesMenu; - [appMenu addItem:servicesItem]; - - [appMenu addItem:[NSMenuItem separatorItem]]; - - // Hide Application - auto hideItem = [[NSMenuItem alloc] initWithTitle:[@"Hide " stringByAppendingString:appName] action:@selector(hide:) keyEquivalent:@"h"]; - - [appMenu addItem:hideItem]; - - // Hide Others - auto hideAllOthersItem = [[NSMenuItem alloc] initWithTitle:@"Hide Others" - action:@selector(hideOtherApplications:) - keyEquivalent:@"h"]; - - hideAllOthersItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagOption; - [appMenu addItem:hideAllOthersItem]; - - // Show All - auto showAllItem = [[NSMenuItem alloc] initWithTitle:@"Show All" - action:@selector(unhideAllApplications:) - keyEquivalent:@""]; - - [appMenu addItem:showAllItem]; - - [appMenu addItem:[NSMenuItem separatorItem]]; - - // Quit Application - auto quitItem = [[NSMenuItem alloc] init]; - quitItem.title = [@"Quit " stringByAppendingString:appName]; - quitItem.keyEquivalent = @"q"; - quitItem.target = [AvnWindow class]; - quitItem.action = @selector(closeAll); - [appMenu addItem:quitItem]; - } } else { @@ -544,6 +497,12 @@ extern void SetAppMenu (NSString* appName, IAvnMenu* menu) } } +extern void SetServicesMenu (IAvnMenu* menu) +{ + auto nativeMenu = dynamic_cast(menu); + [NSApplication sharedApplication].servicesMenu = nativeMenu->GetNative(); +} + extern IAvnMenu* GetAppMenu () { return s_appMenu; diff --git a/src/Avalonia.Controls/Platform/INativeApplicationCommands.cs b/src/Avalonia.Controls/Platform/INativeApplicationCommands.cs new file mode 100644 index 0000000000..6720b8aa2c --- /dev/null +++ b/src/Avalonia.Controls/Platform/INativeApplicationCommands.cs @@ -0,0 +1,12 @@ +namespace Avalonia.Controls.Platform +{ + /// + /// Native Menu Default Application Commands + /// + public interface INativeApplicationCommands + { + void HideApp(); + void ShowAll(); + void HideOthers(); + } +} diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 1582f794ae..b9d8fd3711 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -3,6 +3,7 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Platform; using Avalonia.Dialogs; +using Avalonia.Input; using Avalonia.Native.Interop; using Avalonia.Native.Interop.Impl; using Avalonia.Threading; @@ -18,11 +19,13 @@ namespace Avalonia.Native private NativeMenu _menu; private __MicroComIAvnMenuProxy _nativeMenu; private readonly IAvnTrayIcon _trayIcon; + private readonly IAvnApplicationCommands _applicationCommands; public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) { _factory = factory; _nativeWindow = nativeWindow; + _applicationCommands = _factory.CreateApplicationCommands(); DoLayoutReset(); } @@ -30,6 +33,7 @@ namespace Avalonia.Native public AvaloniaNativeMenuExporter(IAvaloniaNativeFactory factory) { _factory = factory; + _applicationCommands = _factory.CreateApplicationCommands(); DoLayoutReset(); } @@ -38,7 +42,8 @@ namespace Avalonia.Native { _factory = factory; _trayIcon = trayIcon; - + _applicationCommands = _factory.CreateApplicationCommands(); + DoLayoutReset(); } @@ -60,15 +65,11 @@ namespace Avalonia.Native } } - private static NativeMenu CreateDefaultAppMenu() + private NativeMenu CreateDefaultAppMenu() { var result = new NativeMenu(); - var aboutItem = new NativeMenuItem - { - Header = "About Avalonia", - }; - + var aboutItem = new NativeMenuItem("About Avalonia"); aboutItem.Click += async (sender, e) => { var dialog = new AboutAvaloniaDialog(); @@ -77,9 +78,65 @@ namespace Avalonia.Native await dialog.ShowDialog(mainWindow); }; - result.Add(aboutItem); + var macOpts = AvaloniaLocator.Current.GetService(); + if (macOpts == null || !macOpts.DisableDefaultApplicationMenuItems) + { + result.Add(new NativeMenuItemSeparator()); + + var servicesMenu = new NativeMenuItem("Services"); + servicesMenu.Menu = new NativeMenu + { + [MacOSNativeMenuCommands.IsServicesSubmenuProperty] = true + }; + result.Add(servicesMenu); + + result.Add(new NativeMenuItemSeparator()); + + var hideItem = new NativeMenuItem("Hide " + Application.Current.Name) + { + Gesture = new KeyGesture(Key.H, KeyModifiers.Meta) + }; + hideItem.Click += (sender, args) => + { + _applicationCommands.HideApp(); + }; + result.Add(hideItem); + + + var hideOthersItem = new NativeMenuItem("Hide Others") + { + Gesture = new KeyGesture(Key.Q, KeyModifiers.Meta | KeyModifiers.Alt) + }; + hideOthersItem.Click += (sender, args) => + { + _applicationCommands.HideOthers(); + }; + result.Add(hideOthersItem); + + + var showAllItem = new NativeMenuItem("Show All"); + showAllItem.Click += (sender, args) => + { + _applicationCommands.ShowAll(); + }; + result.Add(showAllItem); + + result.Add(new NativeMenuItemSeparator()); + + var quitItem = new NativeMenuItem("Quit") + { + Gesture = new KeyGesture(Key.Q, KeyModifiers.Meta) + }; + quitItem.Click += (sender, args) => + { + _applicationCommands.ShowAll(); + }; + result.Add(quitItem); + } + + return result; } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index eaf4d0e2e4..522db1b334 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -93,8 +93,6 @@ namespace Avalonia.Native var macOpts = AvaloniaLocator.Current.GetService(); _factory.MacOptions.SetShowInDock(macOpts?.ShowInDock != false ? 1 : 0); - _factory.MacOptions.SetDisableDefaultApplicationMenuItems( - macOpts?.DisableDefaultApplicationMenuItems == true ? 1 : 0); } AvaloniaLocator.CurrentMutable @@ -112,7 +110,8 @@ namespace Avalonia.Native .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta, wholeWordTextActionModifiers: KeyModifiers.Alt)) .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()) .Bind().ToConstant(new AvaloniaNativeDragSource(_factory)) - .Bind().ToConstant(applicationPlatform); + .Bind().ToConstant(applicationPlatform) + .Bind().ToConstant(new MacOSNativeMenuCommands(_factory.CreateApplicationCommands())); var hotkeys = AvaloniaLocator.Current.GetService(); hotkeys.MoveCursorToTheStartOfLine.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers)); diff --git a/src/Avalonia.Native/IAvnMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs index ca99cbea4b..de3be6142e 100644 --- a/src/Avalonia.Native/IAvnMenuItem.cs +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -150,6 +150,11 @@ namespace Avalonia.Native.Interop.Impl { _subMenu = __MicroComIAvnMenuProxy.Create(factory); + if (item.Menu.GetValue(MacOSNativeMenuCommands.IsServicesSubmenuProperty)) + { + factory.SetServicesMenu(_subMenu); + } + _subMenu.Initialize(exporter, item.Menu, item.Header); SetSubMenu(_subMenu); diff --git a/src/Avalonia.Native/MacOSNativeMenuCommands.cs b/src/Avalonia.Native/MacOSNativeMenuCommands.cs new file mode 100644 index 0000000000..6d467d307b --- /dev/null +++ b/src/Avalonia.Native/MacOSNativeMenuCommands.cs @@ -0,0 +1,35 @@ +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Native.Interop; + +namespace Avalonia.Native +{ + internal class MacOSNativeMenuCommands : INativeApplicationCommands + { + private readonly IAvnApplicationCommands _commands; + + public MacOSNativeMenuCommands(IAvnApplicationCommands commands) + { + _commands = commands; + } + + public void HideApp() + { + _commands.HideApp(); + } + + public void ShowAll() + { + _commands.ShowAll(); + } + + public void HideOthers() + { + _commands.HideOthers(); + } + + + public static readonly AttachedProperty IsServicesSubmenuProperty = + AvaloniaProperty.RegisterAttached("IsServicesSubmenu", false); + } +} diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 00c54750a4..112b4f636c 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -424,10 +424,12 @@ interface IAvaloniaNativeFactory : IUnknown HRESULT CreateCursorFactory(IAvnCursorFactory** ppv); HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv); HRESULT SetAppMenu(IAvnMenu* menu); + HRESULT SetServicesMenu(IAvnMenu* menu); HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv); HRESULT CreateMenuItem(IAvnMenuItem** ppv); HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv); HRESULT CreateTrayIcon(IAvnTrayIcon** ppv); + HRESULT CreateApplicationCommands(IAvnApplicationCommands** ppv); } [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)] @@ -539,7 +541,6 @@ interface IAvnMacOptions : IUnknown { HRESULT SetShowInDock(int show); HRESULT SetApplicationTitle(char* utf8string); - HRESULT SetDisableDefaultApplicationMenuItems(bool enabled); } [uuid(04c1b049-1f43-418a-9159-cae627ec1367)] @@ -753,3 +754,11 @@ interface IAvnApplicationEvents : IUnknown void FilesOpened (IAvnStringArray* urls); bool TryShutdown(); } + +[uuid(b4284791-055b-4313-8c2e-50f0a8c72ce9)] +interface IAvnApplicationCommands : IUnknown +{ + HRESULT HideApp(); + HRESULT ShowAll(); + HRESULT HideOthers(); +}