diff --git a/Avalonia.sln.DotSettings b/Avalonia.sln.DotSettings index 1361172fff..7060f4a62a 100644 --- a/Avalonia.sln.DotSettings +++ b/Avalonia.sln.DotSettings @@ -3,6 +3,7 @@ ExplicitlyExcluded ExplicitlyExcluded ExplicitlyExcluded + DO_NOT_SHOW HINT <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> diff --git a/global.json b/global.json index 6d12c28846..1e599211d4 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", - "MSBuild.Sdk.Extras": "1.6.65", + "MSBuild.Sdk.Extras": "2.0.46", "AggregatePackage.NuGet.Sdk" : "0.1.12" } } diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index e54f3fa6a7..f1c7664c3e 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -22,6 +22,8 @@ struct IAvnGlContext; struct IAvnGlDisplay; struct IAvnGlSurfaceRenderTarget; struct IAvnGlSurfaceRenderingSession; +struct IAvnAppMenu; +struct IAvnAppMenuItem; struct AvnSize { @@ -173,6 +175,11 @@ public: virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0; virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0; virtual HRESULT ObtainGlFeature(IAvnGlFeature** ppv) = 0; + virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) = 0; + virtual HRESULT SetAppMenu(IAvnAppMenu* menu) = 0; + virtual HRESULT CreateMenu (IAvnAppMenu** ppv) = 0; + virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) = 0; + virtual HRESULT CreateMenuItemSeperator (IAvnAppMenuItem** ppv) = 0; }; AVNCOM(IAvnString, 17) : IUnknown @@ -203,6 +210,8 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown virtual HRESULT SetCursor(IAvnCursor* cursor) = 0; virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ret) = 0; virtual HRESULT GetSoftwareFramebuffer(AvnFramebuffer*ret) = 0; + virtual HRESULT SetMainMenu(IAvnAppMenu* menu) = 0; + virtual HRESULT ObtainMainMenu(IAvnAppMenu** retOut) = 0; virtual bool TryLock() = 0; virtual void Unlock() = 0; }; @@ -258,6 +267,7 @@ AVNCOM(IAvnWindowEvents, 06) : IAvnWindowBaseEvents AVNCOM(IAvnMacOptions, 07) : IUnknown { virtual HRESULT SetShowInDock(int show) = 0; + virtual HRESULT SetApplicationTitle (void* utf8string) = 0; }; AVNCOM(IAvnActionCallback, 08) : IUnknown @@ -367,4 +377,25 @@ AVNCOM(IAvnGlSurfaceRenderingSession, 16) : IUnknown virtual HRESULT GetScaling(double* ret) = 0; }; +AVNCOM(IAvnAppMenu, 17) : IUnknown +{ + virtual HRESULT AddItem (IAvnAppMenuItem* item) = 0; + virtual HRESULT RemoveItem (IAvnAppMenuItem* item) = 0; + virtual HRESULT SetTitle (void* utf8String) = 0; + virtual HRESULT Clear () = 0; +}; + +AVNCOM(IAvnPredicateCallback, 18) : IUnknown +{ + virtual bool Evaluate() = 0; +}; + +AVNCOM(IAvnAppMenuItem, 19) : IUnknown +{ + virtual HRESULT SetSubMenu (IAvnAppMenu* menu) = 0; + virtual HRESULT SetTitle (void* utf8String) = 0; + virtual HRESULT SetGesture (void* utf8String, AvnInputModifiers modifiers) = 0; + virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) = 0; +}; + extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative(); diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 1870ef7ab3..c0a49382a7 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -8,10 +8,12 @@ /* Begin PBXBuildFile section */ 1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; }; + 37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; }; 37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; }; 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; }; 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37DDA9AF219330F8002E132B /* AvnString.mm */; }; 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37E2330E21583241000CB7E2 /* KeyTransform.mm */; }; + 520624B322973F4100C4DCEF /* menu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 520624B222973F4100C4DCEF /* menu.mm */; }; 5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; }; 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; }; AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; }; @@ -24,6 +26,7 @@ /* Begin PBXFileReference section */ 1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = ""; }; + 37155CE3233C00EB0034DCE9 /* menu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = menu.h; sourceTree = ""; }; 379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = ""; }; 37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = ""; }; 37A517B22159597E00FBA241 /* Screens.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = Screens.mm; sourceTree = ""; }; @@ -32,6 +35,7 @@ 37DDA9AF219330F8002E132B /* AvnString.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnString.mm; sourceTree = ""; }; 37DDA9B121933371002E132B /* AvnString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnString.h; sourceTree = ""; }; 37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = ""; }; + 520624B222973F4100C4DCEF /* menu.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = menu.mm; sourceTree = ""; }; 5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = ""; }; 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = ""; }; 5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = ""; }; @@ -85,6 +89,8 @@ AB661C1F2148286E00291242 /* window.mm */, 37C09D8A21581EF2006A6758 /* window.h */, AB00E4F62147CA920032A60A /* main.mm */, + 37155CE3233C00EB0034DCE9 /* menu.h */, + 520624B222973F4100C4DCEF /* menu.mm */, 37A517B22159597E00FBA241 /* Screens.mm */, 37C09D8721580FE4006A6758 /* SystemDialogs.mm */, AB7A61F02147C815003C5833 /* Products */, @@ -107,6 +113,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 37155CE4233C00EB0034DCE9 /* menu.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -150,6 +157,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = AB7A61E62147C814003C5833; @@ -173,6 +181,7 @@ 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */, AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */, 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */, + 520624B322973F4100C4DCEF /* menu.mm in Sources */, 37A517B32159597E00FBA241 /* Screens.mm in Sources */, AB00E4F72147CA920032A60A /* main.mm in Sources */, 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 45ec40c361..10534dea26 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -19,6 +19,13 @@ extern IAvnClipboard* CreateClipboard(); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlFeature* GetGlFeature(); extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* view); +extern IAvnAppMenu* CreateAppMenu(); +extern IAvnAppMenuItem* CreateAppMenuItem(); +extern IAvnAppMenuItem* CreateAppMenuItemSeperator(); +extern void SetAppMenu (NSString* appName, IAvnAppMenu* appMenu); +extern IAvnAppMenu* GetAppMenu (); +extern NSMenuItem* GetAppMenuItem (); + extern void InitializeAvnApp(); extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; extern NSPoint ToNSPoint (AvnPoint p); @@ -40,4 +47,9 @@ template inline T* objc_cast(id from) { return nil; } +@interface ActionCallback : NSObject +- (ActionCallback*) initWithCallback: (IAvnActionCallback*) callback; +- (void) action; +@end + #endif diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 70bd1e67f6..9418782fd1 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -5,10 +5,121 @@ #define COM_GUIDS_MATERIALIZE #include "common.h" +static NSString* s_appTitle = @"Avalonia"; + +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +void SetProcessName(NSString* appTitle) { + s_appTitle = appTitle; + + CFStringRef process_name = (__bridge CFStringRef)appTitle; + + if (!process_name || CFStringGetLength(process_name) == 0) { + //NOTREACHED() << "SetProcessName given bad name."; + return; + } + + if (![NSThread isMainThread]) { + //NOTREACHED() << "Should only set process name from main thread."; + return; + } + + // Warning: here be dragons! This is SPI reverse-engineered from WebKit's + // plugin host, and could break at any time (although realistically it's only + // likely to break in a new major release). + // When 10.7 is available, check that this still works, and update this + // comment for 10.8. + + // Private CFType used in these LaunchServices calls. + typedef CFTypeRef PrivateLSASN; + typedef PrivateLSASN (*LSGetCurrentApplicationASNType)(); + typedef OSStatus (*LSSetApplicationInformationItemType)(int, PrivateLSASN, + CFStringRef, + CFStringRef, + CFDictionaryRef*); + + static LSGetCurrentApplicationASNType ls_get_current_application_asn_func = + NULL; + static LSSetApplicationInformationItemType + ls_set_application_information_item_func = NULL; + static CFStringRef ls_display_name_key = NULL; + + static bool did_symbol_lookup = false; + if (!did_symbol_lookup) { + did_symbol_lookup = true; + CFBundleRef launch_services_bundle = + CFBundleGetBundleWithIdentifier(CFSTR("com.apple.LaunchServices")); + if (!launch_services_bundle) { + //LOG(ERROR) << "Failed to look up LaunchServices bundle"; + return; + } + + ls_get_current_application_asn_func = + reinterpret_cast( + CFBundleGetFunctionPointerForName( + launch_services_bundle, CFSTR("_LSGetCurrentApplicationASN"))); + if (!ls_get_current_application_asn_func){} + //LOG(ERROR) << "Could not find _LSGetCurrentApplicationASN"; + + ls_set_application_information_item_func = + reinterpret_cast( + CFBundleGetFunctionPointerForName( + launch_services_bundle, + CFSTR("_LSSetApplicationInformationItem"))); + if (!ls_set_application_information_item_func){} + //LOG(ERROR) << "Could not find _LSSetApplicationInformationItem"; + + CFStringRef* key_pointer = reinterpret_cast( + CFBundleGetDataPointerForName(launch_services_bundle, + CFSTR("_kLSDisplayNameKey"))); + ls_display_name_key = key_pointer ? *key_pointer : NULL; + if (!ls_display_name_key){} + //LOG(ERROR) << "Could not find _kLSDisplayNameKey"; + + // Internally, this call relies on the Mach ports that are started up by the + // Carbon Process Manager. In debug builds this usually happens due to how + // the logging layers are started up; but in release, it isn't started in as + // much of a defined order. So if the symbols had to be loaded, go ahead + // and force a call to make sure the manager has been initialized and hence + // the ports are opened. + ProcessSerialNumber psn; + GetCurrentProcess(&psn); + } + if (!ls_get_current_application_asn_func || + !ls_set_application_information_item_func || + !ls_display_name_key) { + return; + } + + PrivateLSASN asn = ls_get_current_application_asn_func(); + // Constant used by WebKit; what exactly it means is unknown. + const int magic_session_constant = -2; + OSErr err = + ls_set_application_information_item_func(magic_session_constant, asn, + ls_display_name_key, + process_name, + NULL /* optional out param */); + //LOG_IF(ERROR, err) << "Call to set process name failed, err " << err; +} + class MacOptions : public ComSingleObject { public: FORWARD_IUNKNOWN() + + virtual HRESULT SetApplicationTitle(void* utf8String) override + { + auto appTitle = [NSString stringWithUTF8String:(const char*)utf8String]; + + [[NSProcessInfo processInfo] setProcessName:appTitle]; + + + SetProcessName(appTitle); + + return S_OK; + } + virtual HRESULT SetShowInDock(int show) override { AvnDesiredActivationPolicy = show @@ -17,8 +128,6 @@ public: } }; - - /// See "Using POSIX Threads in a Cocoa Application" section here: /// https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html#//apple_ref/doc/uid/20000738-125024 @interface ThreadingInitializer : NSObject @@ -43,8 +152,6 @@ public: close(_fds[0]); close(_fds[1]); } - - @end @@ -123,6 +230,42 @@ public: *ppv = rv; return S_OK; } + + virtual HRESULT CreateMenu (IAvnAppMenu** ppv) override + { + *ppv = ::CreateAppMenu(); + return S_OK; + } + + virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) override + { + *ppv = ::CreateAppMenuItem(); + return S_OK; + } + + virtual HRESULT CreateMenuItemSeperator (IAvnAppMenuItem** ppv) override + { + *ppv = ::CreateAppMenuItemSeperator(); + return S_OK; + } + + virtual HRESULT SetAppMenu (IAvnAppMenu* appMenu) override + { + ::SetAppMenu(s_appTitle, appMenu); + return S_OK; + } + + virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) override + { + if(retOut == nullptr) + { + return E_POINTER; + } + + *retOut = ::GetAppMenu(); + + return S_OK; + } }; extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative() diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h new file mode 100644 index 0000000000..befbe6a7e0 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -0,0 +1,80 @@ +// +// menu.h +// Avalonia.Native.OSX +// +// Created by Dan Walmsley on 01/08/2019. +// Copyright © 2019 Avalonia. All rights reserved. +// + +#ifndef menu_h +#define menu_h + +#include "common.h" + +class AvnAppMenuItem; +class AvnAppMenu; + +@interface AvnMenu : NSMenu // for some reason it doesnt detect nsmenu here but compiler doesnt complain +- (void)setMenu:(NSMenu*) menu; +@end + +@interface AvnMenuItem : NSMenuItem +- (id) initWithAvnAppMenuItem: (AvnAppMenuItem*)menuItem; +- (void)didSelectItem:(id)sender; +@end + +class AvnAppMenuItem : public ComSingleObject +{ +private: + NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem + IAvnActionCallback* _callback; + IAvnPredicateCallback* _predicate; + bool _isSeperator; + +public: + FORWARD_IUNKNOWN() + + AvnAppMenuItem(bool isSeperator); + + NSMenuItem* GetNative(); + + virtual HRESULT SetSubMenu (IAvnAppMenu* menu) override; + + virtual HRESULT SetTitle (void* utf8String) override; + + virtual HRESULT SetGesture (void* key, AvnInputModifiers modifiers) override; + + virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) override; + + bool EvaluateItemEnabled(); + + void RaiseOnClicked(); +}; + + +class AvnAppMenu : public ComSingleObject +{ +private: + AvnMenu* _native; + +public: + FORWARD_IUNKNOWN() + + AvnAppMenu(); + + AvnAppMenu(AvnMenu* native); + + AvnMenu* GetNative(); + + virtual HRESULT AddItem (IAvnAppMenuItem* item) override; + + virtual HRESULT RemoveItem (IAvnAppMenuItem* item) override; + + virtual HRESULT SetTitle (void* utf8String) override; + + virtual HRESULT Clear () override; +}; + + +#endif + diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm new file mode 100644 index 0000000000..d9dfe36444 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -0,0 +1,305 @@ + +#include "common.h" +#include "menu.h" + +@implementation AvnMenu +@end + +@implementation AvnMenuItem +{ + AvnAppMenuItem* _item; +} + +- (id) initWithAvnAppMenuItem: (AvnAppMenuItem*)menuItem +{ + if(self != nil) + { + _item = menuItem; + self = [super initWithTitle:@"" + action:@selector(didSelectItem:) + keyEquivalent:@""]; + + [self setEnabled:YES]; + + [self setTarget:self]; + } + + return self; +} + +- (BOOL)validateMenuItem:(NSMenuItem *)menuItem +{ + if([self submenu] != nil) + { + return YES; + } + + return _item->EvaluateItemEnabled(); +} + +- (void)didSelectItem:(nullable id)sender +{ + _item->RaiseOnClicked(); +} +@end + +AvnAppMenuItem::AvnAppMenuItem(bool isSeperator) +{ + _isSeperator = isSeperator; + + if(isSeperator) + { + _native = [NSMenuItem separatorItem]; + } + else + { + _native = [[AvnMenuItem alloc] initWithAvnAppMenuItem: this]; + } + + _callback = nullptr; +} + +NSMenuItem* AvnAppMenuItem::GetNative() +{ + return _native; +} + +HRESULT AvnAppMenuItem::SetSubMenu (IAvnAppMenu* menu) +{ + auto nsMenu = dynamic_cast(menu)->GetNative(); + + [_native setSubmenu: nsMenu]; + + return S_OK; +} + +HRESULT AvnAppMenuItem::SetTitle (void* utf8String) +{ + if (utf8String != nullptr) + { + [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + } + + return S_OK; +} + +HRESULT AvnAppMenuItem::SetGesture (void* key, AvnInputModifiers modifiers) +{ + NSEventModifierFlags flags = 0; + + if (modifiers & Control) + flags |= NSEventModifierFlagControl; + if (modifiers & Shift) + flags |= NSEventModifierFlagShift; + if (modifiers & Alt) + flags |= NSEventModifierFlagOption; + if (modifiers & Windows) + flags |= NSEventModifierFlagCommand; + + [_native setKeyEquivalent:[NSString stringWithUTF8String:(const char*)key]]; + [_native setKeyEquivalentModifierMask:flags]; + + return S_OK; +} + +HRESULT AvnAppMenuItem::SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) +{ + _predicate = predicate; + _callback = callback; + return S_OK; +} + +bool AvnAppMenuItem::EvaluateItemEnabled() +{ + if(_predicate != nullptr) + { + auto result = _predicate->Evaluate (); + + return result; + } + + return false; +} + +void AvnAppMenuItem::RaiseOnClicked() +{ + if(_callback != nullptr) + { + _callback->Run(); + } +} + +AvnAppMenu::AvnAppMenu() +{ + _native = [AvnMenu new]; +} + +AvnAppMenu::AvnAppMenu(AvnMenu* native) +{ + _native = native; +} + +AvnMenu* AvnAppMenu::GetNative() +{ + return _native; +} + +HRESULT AvnAppMenu::AddItem (IAvnAppMenuItem* item) +{ + auto avnMenuItem = dynamic_cast(item); + + if(avnMenuItem != nullptr) + { + [_native addItem: avnMenuItem->GetNative()]; + } + + return S_OK; +} + +HRESULT AvnAppMenu::RemoveItem (IAvnAppMenuItem* item) +{ + auto avnMenuItem = dynamic_cast(item); + + if(avnMenuItem != nullptr) + { + [_native removeItem:avnMenuItem->GetNative()]; + } + + return S_OK; +} + +HRESULT AvnAppMenu::SetTitle (void* utf8String) +{ + if (utf8String != nullptr) + { + [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + } + + return S_OK; +} + +HRESULT AvnAppMenu::Clear() +{ + [_native removeAllItems]; + return S_OK; +} + +extern IAvnAppMenu* CreateAppMenu() +{ + @autoreleasepool + { + id menuBar = [NSMenu new]; + return new AvnAppMenu(menuBar); + } +} + +extern IAvnAppMenuItem* CreateAppMenuItem() +{ + @autoreleasepool + { + return new AvnAppMenuItem(false); + } +} + +extern IAvnAppMenuItem* CreateAppMenuItemSeperator() +{ + @autoreleasepool + { + return new AvnAppMenuItem(true); + } +} + +static IAvnAppMenu* s_appMenu = nullptr; +static NSMenuItem* s_appMenuItem = nullptr; + +extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) +{ + s_appMenu = menu; + + if(s_appMenu != nullptr) + { + auto nativeMenu = dynamic_cast(s_appMenu); + + auto currentMenu = [s_appMenuItem menu]; + + if (currentMenu != nullptr) + { + [currentMenu removeItem:s_appMenuItem]; + } + + s_appMenuItem = [nativeMenu->GetNative() itemAtIndex:0]; + + if (currentMenu == nullptr) + { + currentMenu = [s_appMenuItem menu]; + } + + [[s_appMenuItem menu] removeItem:s_appMenuItem]; + + [currentMenu insertItem:s_appMenuItem atIndex:0]; + + if([s_appMenuItem submenu] == nullptr) + { + [s_appMenuItem setSubmenu:[NSMenu new]]; + } + + auto appMenu = [s_appMenuItem submenu]; + + [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.action = @selector(terminate:); + [appMenu addItem:quitItem]; + } + else + { + s_appMenuItem = nullptr; + } +} + +extern IAvnAppMenu* GetAppMenu () +{ + return s_appMenu; +} + +extern NSMenuItem* GetAppMenuItem () +{ + return s_appMenuItem; +} + + diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index 297097584a..e7abedae51 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -10,12 +10,6 @@ class PlatformThreadingInterface; -(Signaler*) init; @end - -@interface ActionCallback : NSObject -- (ActionCallback*) initWithCallback: (IAvnActionCallback*) callback; -- (void) action; -@end - @implementation ActionCallback { ComPtr _callback; diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index e2221217f3..557e19e7a8 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -20,6 +20,7 @@ class WindowBaseImpl; -(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) restoreParentWindow; -(bool) shouldTryToHandleEvents; +-(void) applyMenu:(NSMenu *)menu; @end struct INSWindowHolder diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 3347d58004..dbb437243a 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -5,6 +5,7 @@ #include "window.h" #include "KeyTransform.h" #include "cursor.h" +#include "menu.h" #include class SoftwareDrawingOperation @@ -63,9 +64,11 @@ public: SoftwareDrawingOperation CurrentSwDrawingOperation; AvnPoint lastPositionSet; NSString* _lastTitle; + IAvnAppMenu* _mainMenu; WindowBaseImpl(IAvnWindowBaseEvents* events) { + _mainMenu = nullptr; BaseEvents = events; View = [[AvnView alloc] initWithParent:this]; @@ -93,6 +96,7 @@ public: UpdateStyle(); [Window makeKeyAndOrderFront:Window]; + [NSApp activateIgnoringOtherApps:YES]; [Window setTitle:_lastTitle]; [Window setTitleVisibility:NSWindowTitleVisible]; @@ -122,6 +126,7 @@ public: if(Window != nullptr) { [Window makeKeyWindow]; + [NSApp activateIgnoringOtherApps:YES]; } } @@ -209,6 +214,31 @@ public: } } + virtual HRESULT SetMainMenu(IAvnAppMenu* menu) override + { + _mainMenu = menu; + + auto nativeMenu = dynamic_cast(menu); + + auto nsmenu = nativeMenu->GetNative(); + + [Window applyMenu:nsmenu]; + + return S_OK; + } + + virtual HRESULT ObtainMainMenu(IAvnAppMenu** ret) override + { + if(ret == nullptr) + { + return E_POINTER; + } + + *ret = _mainMenu; + + return S_OK; + } + virtual bool TryLock() override { @autoreleasepool @@ -1042,6 +1072,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent ComPtr _parent; bool _canBecomeKeyAndMain; bool _closed; + NSMenu* _menu; + bool _isAppMenuApplied; } - (void)dealloc @@ -1065,6 +1097,32 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } +-(void) applyMenu:(NSMenu *)menu +{ + if(menu == nullptr) + { + menu = [NSMenu new]; + } + + _menu = menu; + + if ([self isKeyWindow]) + { + auto appMenu = ::GetAppMenuItem(); + + if(appMenu != nullptr) + { + [[appMenu menu] removeItem:appMenu]; + + [_menu insertItem:appMenu atIndex:0]; + + _isAppMenuApplied = true; + } + + [NSApp setMenu:menu]; + } +} + -(void) setCanBecomeKeyAndMain { _canBecomeKeyAndMain = true; @@ -1157,6 +1215,24 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { if([self activateAppropriateChild: true]) { + if(_menu == nullptr) + { + _menu = [NSMenu new]; + } + + auto appMenu = ::GetAppMenuItem(); + + if(appMenu != nullptr) + { + [[appMenu menu] removeItem:appMenu]; + + [_menu insertItem:appMenu atIndex:0]; + + _isAppMenuApplied = true; + } + + [NSApp setMenu:_menu]; + _parent->BaseEvents->Activated(); [super becomeKeyWindow]; } @@ -1201,6 +1277,28 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { if(_parent) _parent->BaseEvents->Deactivated(); + + auto appMenuItem = ::GetAppMenuItem(); + + if(appMenuItem != nullptr) + { + auto appMenu = ::GetAppMenu(); + + auto nativeAppMenu = dynamic_cast(appMenu); + + [[appMenuItem menu] removeItem:appMenuItem]; + + [nativeAppMenu->GetNative() addItem:appMenuItem]; + + [NSApp setMenu:nativeAppMenu->GetNative()]; + } + else + { + [NSApp setMenu:nullptr]; + } + + // remove window menu items from appmenu? + [super resignKeyWindow]; } diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index bc76a39f08..5b82e2caee 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v8.0 + v9.0 Properties\AndroidManifest.xml diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index 157609088f..40d001a195 100644 --- a/samples/ControlCatalog.Android/MainActivity.cs +++ b/samples/ControlCatalog.Android/MainActivity.cs @@ -20,7 +20,7 @@ namespace ControlCatalog.Android { if (Avalonia.Application.Current == null) { - AppBuilder.Configure(new App()) + AppBuilder.Configure() .UseAndroid() .SetupWithoutStarting(); Content = new MainView(); diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index d683092edf..854cae484c 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -55,7 +55,11 @@ namespace ControlCatalog.NetCore public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() - .With(new X11PlatformOptions { EnableMultiTouch = true }) + .With(new X11PlatformOptions + { + EnableMultiTouch = true, + UseDBusMenu = true + }) .With(new Win32PlatformOptions { EnableMultitouch = true, diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 2f6d25c089..335c460b40 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -2,9 +2,9 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.App"> - - - + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 07c42c60c4..4fc63ea054 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -1,4 +1,6 @@ +using System; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; @@ -6,9 +8,20 @@ namespace ControlCatalog { public class App : Application { + private NativeMenu _recentMenu; + public override void Initialize() { AvaloniaXamlLoader.Load(this); + + Name = "Avalonia"; + + _recentMenu = (NativeMenu.GetMenu(this).Items[1] as NativeMenuItem).Menu; + } + + public void OnOpenClicked(object sender, EventArgs args) + { + _recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1))); } public override void OnFrameworkInitializationCompleted() diff --git a/samples/ControlCatalog/DecoratedWindow.xaml b/samples/ControlCatalog/DecoratedWindow.xaml index cb6016b324..8e4c97b7f0 100644 --- a/samples/ControlCatalog/DecoratedWindow.xaml +++ b/samples/ControlCatalog/DecoratedWindow.xaml @@ -3,6 +3,31 @@ x:Class="ControlCatalog.DecoratedWindow" Title="Avalonia Control Gallery" xmlns:local="clr-namespace:ControlCatalog" HasSystemDecorations="False" Name="Window"> + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 9527ac3b4e..6088f2ec57 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -8,7 +8,36 @@ xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:v="clr-namespace:ControlCatalog.Views" x:Class="ControlCatalog.MainWindow"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 95c65ed92f..7b0ee897c4 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -14,6 +14,7 @@ namespace ControlCatalog public class MainWindow : Window { private WindowNotificationManager _notificationArea; + private NativeMenu _recentMenu; public MainWindow() { @@ -29,8 +30,21 @@ namespace ControlCatalog }; DataContext = new MainWindowViewModel(_notificationArea); + _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Menu; } + public void OnOpenClicked(object sender, EventArgs args) + { + _recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1))); + } + + public void OnCloseClicked(object sender, EventArgs args) + { + Close(); + } + + + private void InitializeComponent() { // TODO: iOS does not support dynamically loading assemblies diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml index e1a5cf2c5a..868f0df6ad 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml +++ b/samples/ControlCatalog/Pages/MenuPage.xaml @@ -3,8 +3,11 @@ x:Class="ControlCatalog.Pages.MenuPage"> Menu + Exported menu fallback + (Should be only visible on platforms without desktop-global menu bar) + A window menu - + (this T builder) where T : AppBuilderBase, new() { - builder.UseWindowingSubsystem(() => Android.AndroidPlatform.Initialize(builder.Instance), "Android"); + builder.UseWindowingSubsystem(() => Android.AndroidPlatform.Initialize(builder.ApplicationType), "Android"); builder.UseSkia(); return builder; } @@ -41,7 +41,7 @@ namespace Avalonia.Android _scalingFactor = global::Android.App.Application.Context.Resources.DisplayMetrics.ScaledDensity; } - public static void Initialize(Avalonia.Application app) + public static void Initialize(Type appType) { AvaloniaLocator.CurrentMutable .Bind().ToTransient() @@ -55,7 +55,7 @@ namespace Avalonia.Android .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new RenderLoop()) .Bind().ToSingleton() - .Bind().ToConstant(new AssetLoader(app.GetType().Assembly)); + .Bind().ToConstant(new AssetLoader(appType.Assembly)); SkiaPlatform.Initialize(); ((global::Android.App.Application) global::Android.App.Application.Context.ApplicationContext) diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index 0089ea3b8d..c170e8449c 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -1,6 +1,6 @@  - monoandroid80 + monoandroid90 true diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj index 1b2b205d45..2f95a6e4bd 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v8.0 + v9.0 Properties\AndroidManifest.xml diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 307ddd284c..d9be9171ed 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -18,7 +18,9 @@ namespace Avalonia.Controls { private static bool s_setupWasAlreadyCalled; private Action _optionsInitializers; - + private Func _appFactory; + private IApplicationLifetime _lifetime; + /// /// Gets or sets the instance. /// @@ -30,10 +32,15 @@ namespace Avalonia.Controls public Action RuntimePlatformServicesInitializer { get; private set; } /// - /// Gets or sets the instance being initialized. + /// Gets the instance being initialized. /// - public Application Instance { get; protected set; } - + public Application Instance { get; private set; } + + /// + /// Gets the type of the Instance (even if it's not created yet) + /// + public Type ApplicationType { get; private set; } + /// /// Gets or sets a method to call the initialize the windowing subsystem. /// @@ -76,20 +83,11 @@ namespace Avalonia.Controls public static TAppBuilder Configure() where TApp : Application, new() { - return Configure(new TApp()); - } - - /// - /// Begin configuring an . - /// - /// An instance. - public static TAppBuilder Configure(Application app) - { - AvaloniaLocator.CurrentMutable.BindToSelf(app); - return new TAppBuilder() { - Instance = app, + ApplicationType = typeof(TApp), + // Needed for CoreRT compatibility + _appFactory = () => new TApp() }; } @@ -157,6 +155,18 @@ namespace Avalonia.Controls return Self; } + /// + /// Sets up the platform-specific services for the application and initialized it with a particular lifetime, but does not run it. + /// + /// + /// + public TAppBuilder SetupWithLifetime(IApplicationLifetime lifetime) + { + _lifetime = lifetime; + Setup(); + return Self; + } + /// /// Specifies a windowing subsystem to use. /// @@ -254,11 +264,6 @@ namespace Avalonia.Controls /// private void Setup() { - if (Instance == null) - { - throw new InvalidOperationException("No App instance configured."); - } - if (RuntimePlatformServicesInitializer == null) { throw new InvalidOperationException("No runtime platform services configured."); @@ -285,6 +290,9 @@ namespace Avalonia.Controls WindowingSubsystemInitializer(); RenderingSubsystemInitializer(); AfterPlatformServicesSetupCallback(Self); + Instance = _appFactory(); + Instance.ApplicationLifetime = _lifetime; + AvaloniaLocator.CurrentMutable.BindToSelf(Instance); Instance.RegisterServices(); Instance.Initialize(); AfterSetupCallback(Self); diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 382106de65..ce60a0f0b9 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -32,7 +32,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode + public class Application : AvaloniaObject, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode { /// /// The application-global data templates. @@ -210,5 +210,22 @@ namespace Avalonia { ResourcesChanged?.Invoke(this, e); } + + private string _name; + /// + /// Defines Name property + /// + public static readonly DirectProperty NameProperty = + AvaloniaProperty.RegisterDirect("Name", o => o.Name, (o, v) => o.Name = v); + + /// + /// Application name to be used for various platform-specific purposes + /// + public string Name + { + get => _name; + set => SetAndRaise(NameProperty, ref _name, value); + } + } } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index abca7a64ee..2533191ae4 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -125,8 +125,7 @@ namespace Avalonia where T : AppBuilderBase, new() { var lifetime = new ClassicDesktopStyleApplicationLifetime(builder.Instance) {ShutdownMode = shutdownMode}; - builder.Instance.ApplicationLifetime = lifetime; - builder.SetupWithoutStarting(); + builder.SetupWithLifetime(lifetime); return lifetime.Start(args); } } diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs new file mode 100644 index 0000000000..5d3a4526cc --- /dev/null +++ b/src/Avalonia.Controls/NativeMenu.Export.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls.Platform; +using Avalonia.Data; + +namespace Avalonia.Controls +{ + public partial class NativeMenu + { + public static readonly AttachedProperty IsNativeMenuExportedProperty = + AvaloniaProperty.RegisterAttached("IsNativeMenuExported"); + + public static bool GetIsNativeMenuExported(TopLevel tl) => tl.GetValue(IsNativeMenuExportedProperty); + + private static readonly AttachedProperty s_nativeMenuInfoProperty = + AvaloniaProperty.RegisterAttached("___NativeMenuInfo"); + + class NativeMenuInfo + { + public bool ChangingIsExported { get; set; } + public ITopLevelNativeMenuExporter Exporter { get; } + + public NativeMenuInfo(TopLevel target) + { + Exporter = (target.PlatformImpl as ITopLevelImplWithNativeMenuExporter)?.NativeMenuExporter; + if (Exporter != null) + { + Exporter.OnIsNativeMenuExportedChanged += delegate + { + SetIsNativeMenuExported(target, Exporter.IsNativeMenuExported); + }; + } + } + } + + static NativeMenuInfo GetInfo(TopLevel target) + { + var rv = target.GetValue(s_nativeMenuInfoProperty); + if (rv == null) + { + target.SetValue(s_nativeMenuInfoProperty, rv = new NativeMenuInfo(target)); + SetIsNativeMenuExported(target, rv.Exporter?.IsNativeMenuExported ?? false); + } + + return rv; + } + + static void SetIsNativeMenuExported(TopLevel tl, bool value) + { + GetInfo(tl).ChangingIsExported = true; + tl.SetValue(IsNativeMenuExportedProperty, value); + } + + 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; + }); + + public static void SetMenu(AvaloniaObject o, NativeMenu menu) => o.SetValue(MenuProperty, menu); + public static NativeMenu GetMenu(AvaloniaObject o) => o.GetValue(MenuProperty); + + static NativeMenu() + { + // This is needed because of the lack of attached direct properties + IsNativeMenuExportedProperty.Changed.Subscribe(args => + { + var info = GetInfo((TopLevel)args.Sender); + if (!info.ChangingIsExported) + throw new InvalidOperationException("IsNativeMenuExported property is read-only"); + info.ChangingIsExported = false; + }); + MenuProperty.Changed.Subscribe(args => + { + if (args.Sender is TopLevel tl) + { + GetInfo(tl).Exporter?.SetNativeMenu((NativeMenu)args.NewValue); + } + }); + } + } +} diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs new file mode 100644 index 0000000000..54aa2b5e3d --- /dev/null +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using Avalonia.Collections; +using Avalonia.Data; +using Avalonia.LogicalTree; +using Avalonia.Metadata; + +namespace Avalonia.Controls +{ + public partial class NativeMenu : AvaloniaObject, IEnumerable + { + private readonly AvaloniaList _items = + new AvaloniaList { ResetBehavior = ResetBehavior.Remove }; + private NativeMenuItem _parent; + [Content] + public IList Items => _items; + + public NativeMenu() + { + _items.Validate = Validator; + _items.CollectionChanged += ItemsChanged; + } + + private void Validator(NativeMenuItemBase obj) + { + if (obj.Parent != null) + throw new InvalidOperationException("NativeMenuItem already has a parent"); + } + + private void ItemsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if(e.OldItems!=null) + foreach (NativeMenuItemBase i in e.OldItems) + i.Parent = null; + if(e.NewItems!=null) + foreach (NativeMenuItemBase i in e.NewItems) + i.Parent = this; + } + + public static readonly DirectProperty ParentProperty = + AvaloniaProperty.RegisterDirect("Parent", o => o.Parent, (o, v) => o.Parent = v); + + public NativeMenuItem Parent + { + get => _parent; + set => SetAndRaise(ParentProperty, ref _parent, value); + } + + public void Add(NativeMenuItemBase item) => _items.Add(item); + + public IEnumerator GetEnumerator() => _items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Avalonia.Controls/NativeMenuBar.cs b/src/Avalonia.Controls/NativeMenuBar.cs new file mode 100644 index 0000000000..9b96ab9c8c --- /dev/null +++ b/src/Avalonia.Controls/NativeMenuBar.cs @@ -0,0 +1,36 @@ +using System; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Styling; + +namespace Avalonia.Controls +{ + public class NativeMenuBar : TemplatedControl + { + public static readonly AttachedProperty EnableMenuItemClickForwardingProperty = + AvaloniaProperty.RegisterAttached( + "EnableMenuItemClickForwarding"); + + static NativeMenuBar() + { + EnableMenuItemClickForwardingProperty.Changed.Subscribe(args => + { + var item = (MenuItem)args.Sender; + if (args.NewValue.Equals(true)) + item.Click += OnMenuItemClick; + else + item.Click -= OnMenuItemClick; + }); + } + + public static void SetEnableMenuItemClickForwarding(MenuItem menuItem, bool enable) + { + menuItem.SetValue(EnableMenuItemClickForwardingProperty, enable); + } + + private static void OnMenuItemClick(object sender, RoutedEventArgs e) + { + (((MenuItem)sender).DataContext as NativeMenuItem)?.RaiseClick(); + } + } +} diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs new file mode 100644 index 0000000000..c1144d45b2 --- /dev/null +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -0,0 +1,162 @@ +using System; +using System.Windows.Input; +using Avalonia.Input; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + public class NativeMenuItem : NativeMenuItemBase + { + private string _header; + private KeyGesture _gesture; + private bool _enabled = true; + + private NativeMenu _menu; + + static NativeMenuItem() + { + MenuProperty.Changed.Subscribe(args => + { + var item = (NativeMenuItem)args.Sender; + var value = (NativeMenu)args.NewValue; + if (value.Parent != null && value.Parent != item) + throw new InvalidOperationException("NativeMenu already has a parent"); + value.Parent = item; + }); + } + + + class CanExecuteChangedSubscriber : IWeakSubscriber + { + private readonly NativeMenuItem _parent; + + public CanExecuteChangedSubscriber(NativeMenuItem parent) + { + _parent = parent; + } + + public void OnEvent(object sender, EventArgs e) + { + _parent.CanExecuteChanged(); + } + } + + private readonly CanExecuteChangedSubscriber _canExecuteChangedSubscriber; + + + public NativeMenuItem() + { + _canExecuteChangedSubscriber = new CanExecuteChangedSubscriber(this); + } + + public NativeMenuItem(string header) : this() + { + Header = header; + } + + public static readonly DirectProperty MenuProperty = + AvaloniaProperty.RegisterDirect(nameof(Menu), o => o._menu, + (o, v) => + { + if (v.Parent != null && v.Parent != o) + throw new InvalidOperationException("NativeMenu already has a parent"); + o._menu = v; + }); + + public NativeMenu Menu + { + get => _menu; + set + { + if (value.Parent != null && value.Parent != this) + throw new InvalidOperationException("NativeMenu already has a parent"); + SetAndRaise(MenuProperty, ref _menu, value); + } + } + + public static readonly DirectProperty HeaderProperty = + AvaloniaProperty.RegisterDirect(nameof(Header), o => o._header, (o, v) => o._header = v); + + public string Header + { + get => GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); + } + + public static readonly DirectProperty GestureProperty = + AvaloniaProperty.RegisterDirect(nameof(Gesture), o => o._gesture, (o, v) => o._gesture = v); + + public KeyGesture Gesture + { + get => GetValue(GestureProperty); + set => SetValue(GestureProperty, value); + } + + private ICommand _command; + + public static readonly DirectProperty CommandProperty = + AvaloniaProperty.RegisterDirect(nameof(Command), + o => o._command, (o, v) => + { + if (o._command != null) + WeakSubscriptionManager.Unsubscribe(o._command, + nameof(ICommand.CanExecuteChanged), o._canExecuteChangedSubscriber); + o._command = v; + if (o._command != null) + WeakSubscriptionManager.Subscribe(o._command, + nameof(ICommand.CanExecuteChanged), o._canExecuteChangedSubscriber); + o.CanExecuteChanged(); + }); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CommandParameterProperty = + Button.CommandParameterProperty.AddOwner(); + + public static readonly DirectProperty EnabledProperty = + AvaloniaProperty.RegisterDirect(nameof(Enabled), o => o._enabled, + (o, v) => o._enabled = v, true); + + public bool Enabled + { + get => GetValue(EnabledProperty); + set => SetValue(EnabledProperty, value); + } + + void CanExecuteChanged() + { + Enabled = _command?.CanExecute(null) ?? true; + } + + public bool HasClickHandlers => Clicked != null; + + public ICommand Command + { + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); + } + + /// + /// Gets or sets the parameter to pass to the property of a + /// . + /// + public object CommandParameter + { + get { return GetValue(CommandParameterProperty); } + set { SetValue(CommandParameterProperty, value); } + } + + public event EventHandler Clicked; + + public void RaiseClick() + { + Clicked?.Invoke(this, new EventArgs()); + + if (Command?.CanExecute(CommandParameter) == true) + { + Command.Execute(CommandParameter); + } + } + } +} diff --git a/src/Avalonia.Controls/NativeMenuItemBase.cs b/src/Avalonia.Controls/NativeMenuItemBase.cs new file mode 100644 index 0000000000..47eb86cdc3 --- /dev/null +++ b/src/Avalonia.Controls/NativeMenuItemBase.cs @@ -0,0 +1,23 @@ +using System; + +namespace Avalonia.Controls +{ + public class NativeMenuItemBase : AvaloniaObject + { + private NativeMenu _parent; + + internal NativeMenuItemBase() + { + + } + + public static readonly DirectProperty ParentProperty = + AvaloniaProperty.RegisterDirect("Parent", o => o.Parent, (o, v) => o.Parent = v); + + public NativeMenu Parent + { + get => _parent; + set => SetAndRaise(ParentProperty, ref _parent, value); + } + } +} diff --git a/src/Avalonia.Controls/NativeMenuItemSeperator.cs b/src/Avalonia.Controls/NativeMenuItemSeperator.cs new file mode 100644 index 0000000000..e743483dab --- /dev/null +++ b/src/Avalonia.Controls/NativeMenuItemSeperator.cs @@ -0,0 +1,10 @@ +using System; + +namespace Avalonia.Controls +{ + public class NativeMenuItemSeperator : NativeMenuItemBase + { + [Obsolete("This is a temporary hack to make our MenuItem recognize this as a separator, don't use", true)] + public string Header => "-"; + } +} diff --git a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs new file mode 100644 index 0000000000..3ac5f28956 --- /dev/null +++ b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using Avalonia.Platform; + +namespace Avalonia.Controls.Platform +{ + public interface ITopLevelNativeMenuExporter + { + bool IsNativeMenuExported { get; } + event EventHandler OnIsNativeMenuExportedChanged; + void SetNativeMenu(NativeMenu menu); + } + + public interface ITopLevelImplWithNativeMenuExporter : ITopLevelImpl + { + ITopLevelNativeMenuExporter NativeMenuExporter { get; } + } +} diff --git a/src/Avalonia.DesktopRuntime/AppBuilder.cs b/src/Avalonia.DesktopRuntime/AppBuilder.cs index dbe3767df6..ff0d84a6e9 100644 --- a/src/Avalonia.DesktopRuntime/AppBuilder.cs +++ b/src/Avalonia.DesktopRuntime/AppBuilder.cs @@ -18,19 +18,10 @@ namespace Avalonia /// public AppBuilder() : base(new StandardRuntimePlatform(), - builder => StandardRuntimePlatformServices.Register(builder.Instance?.GetType()?.Assembly)) + builder => StandardRuntimePlatformServices.Register(builder.ApplicationType.Assembly)) { } - /// - /// Initializes a new instance of the class. - /// - /// The instance. - public AppBuilder(Application app) : this() - { - Instance = app; - } - bool CheckEnvironment(Type checkerType) { if (checkerType == null) diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index d7e1d8cdb3..d9fd3b78a2 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -8,4 +8,8 @@ + + + + diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs new file mode 100644 index 0000000000..b445f86613 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -0,0 +1,82 @@ +using System; +using System.Threading; +using Avalonia.Threading; +using Tmds.DBus; + +namespace Avalonia.FreeDesktop +{ + public class DBusHelper + { + /// + /// This class uses synchronous execution at DBus connection establishment stage + /// then switches to using AvaloniaSynchronizationContext + /// + class DBusSyncContext : SynchronizationContext + { + private SynchronizationContext _ctx; + private object _lock = new object(); + + public override void Post(SendOrPostCallback d, object state) + { + lock (_lock) + { + if (_ctx != null) + _ctx?.Post(d, state); + else + lock (_lock) + d(state); + } + } + + public override void Send(SendOrPostCallback d, object state) + { + lock (_lock) + { + if (_ctx != null) + _ctx?.Send(d, state); + else + + d(state); + } + } + + public void Initialized() + { + lock (_lock) + _ctx = new AvaloniaSynchronizationContext(); + } + } + public static Connection Connection { get; private set; } + + public static Exception TryInitialize(string dbusAddress = null) + { + var oldContext = SynchronizationContext.Current; + try + { + + var dbusContext = new DBusSyncContext(); + SynchronizationContext.SetSynchronizationContext(dbusContext); + var conn = new Connection(new ClientConnectionOptions(dbusAddress ?? Address.Session) + { + AutoConnect = false, + SynchronizationContext = dbusContext + }); + // Connect synchronously + conn.ConnectAsync().Wait(); + + // Initialize a brand new sync-context + dbusContext.Initialized(); + Connection = conn; + } + catch (Exception e) + { + return e; + } + finally + { + SynchronizationContext.SetSynchronizationContext(oldContext); + } + return null; + } + } +} diff --git a/src/Avalonia.FreeDesktop/DBusMenu.cs b/src/Avalonia.FreeDesktop/DBusMenu.cs new file mode 100644 index 0000000000..7180345386 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusMenu.cs @@ -0,0 +1,56 @@ + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Tmds.DBus; + +[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] +namespace Avalonia.FreeDesktop.DBusMenu +{ + + [DBusInterface("org.freedesktop.DBus.Properties")] + interface IFreeDesktopDBusProperties : IDBusObject + { + Task GetAsync(string prop); + Task GetAllAsync(); + Task SetAsync(string prop, object val); + Task WatchPropertiesAsync(Action handler); + } + + [DBusInterface("com.canonical.dbusmenu")] + interface IDBusMenu : IFreeDesktopDBusProperties + { + Task<(uint revision, (int, KeyValuePair[], object[]) layout)> GetLayoutAsync(int ParentId, int RecursionDepth, string[] PropertyNames); + Task<(int, KeyValuePair[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames); + Task GetPropertyAsync(int Id, string Name); + Task EventAsync(int Id, string EventId, object Data, uint Timestamp); + Task EventGroupAsync((int id, string eventId, object data, uint timestamp)[] events); + Task AboutToShowAsync(int Id); + Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids); + Task WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action onError = null); + Task WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action onError = null); + Task WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action onError = null); + } + + [Dictionary] + class DBusMenuProperties + { + public uint Version { get; set; } = default (uint); + public string TextDirection { get; set; } = default (string); + public string Status { get; set; } = default (string); + public string[] IconThemePath { get; set; } = default (string[]); + } + + + [DBusInterface("com.canonical.AppMenu.Registrar")] + interface IRegistrar : IDBusObject + { + Task RegisterWindowAsync(uint WindowId, ObjectPath MenuObjectPath); + Task UnregisterWindowAsync(uint WindowId); + Task<(string service, ObjectPath menuObjectPath)> GetMenuForWindowAsync(uint WindowId); + Task<(uint, string, ObjectPath)[]> GetMenusAsync(); + Task WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action onError = null); + Task WatchWindowUnregisteredAsync(Action handler, Action onError = null); + } +} diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs new file mode 100644 index 0000000000..90239b5a49 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -0,0 +1,387 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Reactive.Disposables; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.FreeDesktop.DBusMenu; +using Avalonia.Input; +using Avalonia.Threading; +using Tmds.DBus; +#pragma warning disable 1998 + +namespace Avalonia.FreeDesktop +{ + public class DBusMenuExporter + { + public static ITopLevelNativeMenuExporter TryCreate(IntPtr xid) + { + if (DBusHelper.Connection == null) + return null; + + return new DBusMenuExporterImpl(DBusHelper.Connection, xid); + } + + class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable + { + private readonly Connection _dbus; + private readonly uint _xid; + private IRegistrar _registar; + private bool _disposed; + private uint _revision = 1; + private NativeMenu _menu; + private Dictionary _idsToItems = new Dictionary(); + private Dictionary _itemsToIds = new Dictionary(); + private readonly HashSet _menus = new HashSet(); + private bool _resetQueued; + private int _nextId = 1; + public DBusMenuExporterImpl(Connection dbus, IntPtr xid) + { + _dbus = dbus; + _xid = (uint)xid.ToInt32(); + ObjectPath = new ObjectPath("/net/avaloniaui/dbusmenu/" + + Guid.NewGuid().ToString().Replace("-", "")); + SetNativeMenu(new NativeMenu()); + Init(); + } + + async void Init() + { + try + { + await _dbus.RegisterObjectAsync(this); + _registar = DBusHelper.Connection.CreateProxy( + "com.canonical.AppMenu.Registrar", + "/com/canonical/AppMenu/Registrar"); + if (!_disposed) + await _registar.RegisterWindowAsync(_xid, ObjectPath); + } + catch (Exception e) + { + Console.Error.WriteLine(e); + // It's not really important if this code succeeds, + // and it's not important to know if it succeeds + // since even if we register the window it's not guaranteed that + // menu will be actually exported + } + } + + public void Dispose() + { + if (_disposed) + return; + _disposed = true; + _dbus.UnregisterObject(this); + // Fire and forget + _registar?.UnregisterWindowAsync(_xid); + } + + + + public bool IsNativeMenuExported { get; private set; } + public event EventHandler OnIsNativeMenuExportedChanged; + + public void SetNativeMenu(NativeMenu menu) + { + if (menu == null) + menu = new NativeMenu(); + + if (_menu != null) + ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged; + _menu = menu; + ((INotifyCollectionChanged)_menu.Items).CollectionChanged += OnMenuItemsChanged; + + DoLayoutReset(); + } + + /* + This is basic initial implementation, so we don't actually track anything and + just reset the whole layout on *ANY* change + + This is not how it should work and will prevent us from implementing various features, + but that's the fastest way to get things working, so... + */ + void DoLayoutReset() + { + _resetQueued = false; + foreach (var i in _idsToItems.Values) + i.PropertyChanged -= OnItemPropertyChanged; + foreach(var menu in _menus) + ((INotifyCollectionChanged)menu.Items).CollectionChanged -= OnMenuItemsChanged; + _menus.Clear(); + _idsToItems.Clear(); + _itemsToIds.Clear(); + _revision++; + LayoutUpdated?.Invoke((_revision, 0)); + } + + void QueueReset() + { + if(_resetQueued) + return; + _resetQueued = true; + Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); + } + + private (NativeMenuItemBase item, NativeMenu menu) GetMenu(int id) + { + if (id == 0) + return (null, _menu); + _idsToItems.TryGetValue(id, out var item); + return (item, (item as NativeMenuItem)?.Menu); + } + + private void EnsureSubscribed(NativeMenu menu) + { + if(menu!=null && _menus.Add(menu)) + ((INotifyCollectionChanged)menu.Items).CollectionChanged += OnMenuItemsChanged; + } + + private int GetId(NativeMenuItemBase item) + { + if (_itemsToIds.TryGetValue(item, out var id)) + return id; + id = _nextId++; + _idsToItems[id] = item; + _itemsToIds[item] = id; + item.PropertyChanged += OnItemPropertyChanged; + if (item is NativeMenuItem nmi) + EnsureSubscribed(nmi.Menu); + return id; + } + + private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + QueueReset(); + } + + private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + QueueReset(); + } + + public ObjectPath ObjectPath { get; } + + + async Task IFreeDesktopDBusProperties.GetAsync(string prop) + { + if (prop == "Version") + return 2; + if (prop == "Status") + return "normal"; + return 0; + } + + async Task IFreeDesktopDBusProperties.GetAllAsync() + { + return new DBusMenuProperties + { + Version = 2, + Status = "normal", + }; + } + + private static string[] AllProperties = new[] + { + "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display" + }; + + object GetProperty((NativeMenuItemBase item, NativeMenu menu) i, string name) + { + var (it, menu) = i; + + if (it is NativeMenuItemSeperator) + { + if (name == "type") + return "separator"; + } + else if (it is NativeMenuItem item) + { + if (name == "type") + { + return null; + } + if (name == "label") + return item?.Header ?? ""; + if (name == "enabled") + { + if (item == null) + return null; + if (item.Menu != null && item.Menu.Items.Count == 0) + return false; + if (item.Enabled == false) + return false; + return null; + } + if (name == "shortcut") + { + if (item?.Gesture == null) + return null; + if (item.Gesture.KeyModifiers == 0) + return null; + var lst = new List(); + var mod = item.Gesture; + if ((mod.KeyModifiers & KeyModifiers.Control) != 0) + lst.Add("Control"); + if ((mod.KeyModifiers & KeyModifiers.Alt) != 0) + lst.Add("Alt"); + if ((mod.KeyModifiers & KeyModifiers.Shift) != 0) + lst.Add("Shift"); + if ((mod.KeyModifiers & KeyModifiers.Meta) != 0) + lst.Add("Super"); + lst.Add(item.Gesture.Key.ToString()); + return new[] { lst.ToArray() }; + } + + if (name == "children-display") + return menu != null ? "submenu" : null; + } + + return null; + } + + private List> _reusablePropertyList = new List>(); + KeyValuePair[] GetProperties((NativeMenuItemBase item, NativeMenu menu) i, string[] names) + { + if (names?.Length > 0 != true) + names = AllProperties; + _reusablePropertyList.Clear(); + foreach (var n in names) + { + var v = GetProperty(i, n); + if (v != null) + _reusablePropertyList.Add(new KeyValuePair(n, v)); + } + + return _reusablePropertyList.ToArray(); + } + + + public Task SetAsync(string prop, object val) => Task.CompletedTask; + + public Task<(uint revision, (int, KeyValuePair[], object[]) layout)> GetLayoutAsync( + int ParentId, int RecursionDepth, string[] PropertyNames) + { + var menu = GetMenu(ParentId); + var rv = (_revision, GetLayout(menu.item, menu.menu, RecursionDepth, PropertyNames)); + if (!IsNativeMenuExported) + { + IsNativeMenuExported = true; + Dispatcher.UIThread.Post(() => + { + OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty); + }); + } + return Task.FromResult(rv); + } + + (int, KeyValuePair[], object[]) GetLayout(NativeMenuItemBase item, NativeMenu menu, int depth, string[] propertyNames) + { + var id = item == null ? 0 : GetId(item); + var props = GetProperties((item, menu), propertyNames); + var children = (depth == 0 || menu == null) ? new object[0] : new object[menu.Items.Count]; + if(menu != null) + for (var c = 0; c < children.Length; c++) + { + var ch = menu.Items[c]; + + children[c] = GetLayout(ch, (ch as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames); + } + + return (id, props, children); + } + + public Task<(int, KeyValuePair[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames) + { + var arr = new (int, KeyValuePair[])[Ids.Length]; + for (var c = 0; c < Ids.Length; c++) + { + var id = Ids[c]; + var item = GetMenu(id); + var props = GetProperties(item, PropertyNames); + arr[c] = (id, props); + } + + return Task.FromResult(arr); + } + + public async Task GetPropertyAsync(int Id, string Name) + { + return GetProperty(GetMenu(Id), Name) ?? 0; + } + + + + public void HandleEvent(int id, string eventId, object data, uint timestamp) + { + if (eventId == "clicked") + { + var item = GetMenu(id).item; + + if (item is NativeMenuItem menuItem) + { + if (menuItem?.Enabled == true) + menuItem.RaiseClick(); + } + } + } + + public Task EventAsync(int Id, string EventId, object Data, uint Timestamp) + { + HandleEvent(Id, EventId, Data, Timestamp); + return Task.CompletedTask; + } + + public Task EventGroupAsync((int id, string eventId, object data, uint timestamp)[] Events) + { + foreach (var e in Events) + HandleEvent(e.id, e.eventId, e.data, e.timestamp); + return Task.FromResult(new int[0]); + } + + public async Task AboutToShowAsync(int Id) + { + return false; + } + + public async Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids) + { + return (new int[0], new int[0]); + } + + #region Events + + private event Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> + ItemsPropertiesUpdated; + private event Action<(uint revision, int parent)> LayoutUpdated; + private event Action<(int id, uint timestamp)> ItemActivationRequested; + private event Action PropertiesChanged; + + async Task IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action onError) + { + ItemsPropertiesUpdated += handler; + return Disposable.Create(() => ItemsPropertiesUpdated -= handler); + } + async Task IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action onError) + { + LayoutUpdated += handler; + return Disposable.Create(() => LayoutUpdated -= handler); + } + + async Task IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action onError) + { + ItemActivationRequested+= handler; + return Disposable.Create(() => ItemActivationRequested -= handler); + } + + async Task IFreeDesktopDBusProperties.WatchPropertiesAsync(Action handler) + { + PropertiesChanged += handler; + return Disposable.Create(() => PropertiesChanged -= handler); + } + + #endregion + } + } +} diff --git a/src/Avalonia.Input/KeyGesture.cs b/src/Avalonia.Input/KeyGesture.cs index 5eaee4833c..490c31bef9 100644 --- a/src/Avalonia.Input/KeyGesture.cs +++ b/src/Avalonia.Input/KeyGesture.cs @@ -14,7 +14,7 @@ namespace Avalonia.Input { private static readonly Dictionary s_keySynonyms = new Dictionary { - { "+", Key.OemPlus }, { "-", Key.OemMinus }, { ".", Key.OemPeriod } + { "+", Key.OemPlus }, { "-", Key.OemMinus }, { ".", Key.OemPeriod }, { ",", Key.OemComma } }; [Obsolete("Use constructor taking KeyModifiers")] @@ -141,6 +141,11 @@ namespace Avalonia.Input return KeyModifiers.Control; } + if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + return KeyModifiers.Meta; + } + return (KeyModifiers)Enum.Parse(typeof(KeyModifiers), modifier.ToString(), true); } diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs new file mode 100644 index 0000000000..1a22b95409 --- /dev/null +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -0,0 +1,305 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Native.Interop; +using Avalonia.Platform.Interop; +using Avalonia.Threading; + +namespace Avalonia.Native +{ + public class MenuActionCallback : CallbackBase, IAvnActionCallback + { + private Action _action; + + public MenuActionCallback(Action action) + { + _action = action; + } + + void IAvnActionCallback.Run() + { + _action?.Invoke(); + } + } + + public class PredicateCallback : CallbackBase, IAvnPredicateCallback + { + private Func _predicate; + + public PredicateCallback(Func predicate) + { + _predicate = predicate; + } + + bool IAvnPredicateCallback.Evaluate() + { + return _predicate(); + } + } + + class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter + { + private IAvaloniaNativeFactory _factory; + private NativeMenu _menu; + private bool _resetQueued; + private bool _exported = false; + private IAvnWindow _nativeWindow; + private List _menuItems = new List(); + + public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) + { + _factory = factory; + _nativeWindow = nativeWindow; + + DoLayoutReset(); + } + + public AvaloniaNativeMenuExporter(IAvaloniaNativeFactory factory) + { + _factory = factory; + + _menu = NativeMenu.GetMenu(Application.Current); + DoLayoutReset(); + } + + public bool IsNativeMenuExported => _exported; + + public event EventHandler OnIsNativeMenuExportedChanged; + + public void SetNativeMenu(NativeMenu menu) + { + if (menu == null) + menu = new NativeMenu(); + + if (_menu != null) + ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged; + _menu = menu; + ((INotifyCollectionChanged)_menu.Items).CollectionChanged += OnMenuItemsChanged; + + DoLayoutReset(); + } + + private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + QueueReset(); + } + + private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + QueueReset(); + } + + void DoLayoutReset() + { + _resetQueued = false; + foreach (var i in _menuItems) + { + i.PropertyChanged -= OnItemPropertyChanged; + if (i.Menu != null) + ((INotifyCollectionChanged)i.Menu.Items).CollectionChanged -= OnMenuItemsChanged; + } + + _menuItems.Clear(); + + if(_nativeWindow is null) + { + _menu = NativeMenu.GetMenu(Application.Current); + + if(_menu != null) + { + SetMenu(_menu); + } + } + else + { + SetMenu(_nativeWindow, _menu?.Items); + } + + _exported = true; + } + + private void QueueReset() + { + if (_resetQueued) + return; + _resetQueued = true; + Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); + } + + private IAvnAppMenu CreateSubmenu(ICollection children) + { + var menu = _factory.CreateMenu(); + + SetChildren(menu, children); + + return menu; + } + + private void AddMenuItem(NativeMenuItem item) + { + if (item.Menu?.Items != null) + { + ((INotifyCollectionChanged)item.Menu.Items).CollectionChanged += OnMenuItemsChanged; + } + } + + private void SetChildren(IAvnAppMenu menu, ICollection children) + { + foreach (var i in children) + { + if (i is NativeMenuItem item) + { + AddMenuItem(item); + + var menuItem = _factory.CreateMenuItem(); + + using (var buffer = new Utf8Buffer(item.Header)) + { + menuItem.Title = buffer.DangerousGetHandle(); + } + + if (item.Gesture != null) + { + using (var buffer = new Utf8Buffer(item.Gesture.Key.ToString().ToLower())) + { + menuItem.SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); + } + } + + menuItem.SetAction(new PredicateCallback(() => + { + if (item.Command != null || item.HasClickHandlers) + { + return item.Enabled; + } + + return false; + }), new MenuActionCallback(() => { item.RaiseClick(); })); + menu.AddItem(menuItem); + + if (item.Menu?.Items?.Count > 0) + { + var submenu = _factory.CreateMenu(); + + using (var buffer = new Utf8Buffer(item.Header)) + { + submenu.Title = buffer.DangerousGetHandle(); + } + + menuItem.SetSubMenu(submenu); + + AddItemsToMenu(submenu, item.Menu?.Items); + } + } + else if (i is NativeMenuItemSeperator seperator) + { + menu.AddItem(_factory.CreateMenuItemSeperator()); + } + } + } + + private void AddItemsToMenu(IAvnAppMenu menu, ICollection items, bool isMainMenu = false) + { + foreach (var i in items) + { + if (i is NativeMenuItem item) + { + var menuItem = _factory.CreateMenuItem(); + + AddMenuItem(item); + + menuItem.SetAction(new PredicateCallback(() => + { + if (item.Command != null || item.HasClickHandlers) + { + return item.Enabled; + } + + return false; + }), new MenuActionCallback(() => { item.RaiseClick(); })); + + if (item.Menu?.Items.Count > 0 || isMainMenu) + { + var subMenu = CreateSubmenu(item.Menu?.Items); + + menuItem.SetSubMenu(subMenu); + + using (var buffer = new Utf8Buffer(item.Header)) + { + subMenu.Title = buffer.DangerousGetHandle(); + } + } + else + { + using (var buffer = new Utf8Buffer(item.Header)) + { + menuItem.Title = buffer.DangerousGetHandle(); + } + + if (item.Gesture != null) + { + using (var buffer = new Utf8Buffer(item.Gesture.Key.ToString().ToLower())) + { + menuItem.SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); + } + } + } + + menu.AddItem(menuItem); + } + else if(i is NativeMenuItemSeperator seperator) + { + menu.AddItem(_factory.CreateMenuItemSeperator()); + } + } + } + + private void SetMenu(NativeMenu menu) + { + var appMenu = _factory.ObtainAppMenu(); + + if (appMenu is null) + { + appMenu = _factory.CreateMenu(); + } + + var menuItem = menu.Parent; + + if(menu.Parent is null) + { + menuItem = new NativeMenuItem(); + } + + menuItem.Menu = menu; + + appMenu.Clear(); + AddItemsToMenu(appMenu, new List { menuItem }); + + _factory.SetAppMenu(appMenu); + } + + private void SetMenu(IAvnWindow avnWindow, ICollection menuItems) + { + if (menuItems is null) + { + menuItems = new List(); + } + + var appMenu = avnWindow.ObtainMainMenu(); + + if (appMenu is null) + { + appMenu = _factory.CreateMenu(); + } + + appMenu.Clear(); + AddItemsToMenu(appMenu, menuItems); + + avnWindow.SetMainMenu(appMenu); + } + } +} diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 6d48ab3829..ddb71b61bb 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -9,6 +9,7 @@ using Avalonia.Native.Interop; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Platform.Interop; namespace Avalonia.Native { @@ -27,15 +28,17 @@ namespace Avalonia.Native public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(500); //TODO - public static void Initialize(IntPtr factory, AvaloniaNativePlatformOptions options) + public static AvaloniaNativePlatform Initialize(IntPtr factory, AvaloniaNativePlatformOptions options) { - new AvaloniaNativePlatform(new IAvaloniaNativeFactory(factory)) - .DoInitialize(options); + var result = new AvaloniaNativePlatform(new IAvaloniaNativeFactory(factory)); + result.DoInitialize(options); + + return result; } delegate IntPtr CreateAvaloniaNativeDelegate(); - public static void Initialize(AvaloniaNativePlatformOptions options) + public static AvaloniaNativePlatform Initialize(AvaloniaNativePlatformOptions options) { if (options.AvaloniaNativeLibraryPath != null) { @@ -48,10 +51,26 @@ namespace Avalonia.Native var d = Marshal.GetDelegateForFunctionPointer(proc); - Initialize(d(), options); + return Initialize(d(), options); } else - Initialize(CreateAvaloniaNative(), options); + return Initialize(CreateAvaloniaNative(), options); + } + + public void SetupApplicationMenuExporter () + { + var exporter = new AvaloniaNativeMenuExporter(_factory); + } + + public void SetupApplicationName () + { + if(!string.IsNullOrWhiteSpace(Application.Current.Name)) + { + using (var buffer = new Utf8Buffer(Application.Current.Name)) + { + _factory.MacOptions.SetApplicationTitle(buffer.DangerousGetHandle()); + } + } } private AvaloniaNativePlatform(IAvaloniaNativeFactory factory) @@ -66,6 +85,7 @@ namespace Avalonia.Native if (_factory.MacOptions != null) { var macOpts = AvaloniaLocator.Current.GetService(); + _factory.MacOptions.ShowInDock = macOpts?.ShowInDock != false ? 1 : 0; } diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index 02810ed155..091056142f 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -13,9 +13,18 @@ namespace Avalonia where T : AppBuilderBase, new() { builder.UseWindowingSubsystem(() => - AvaloniaNativePlatform.Initialize( + { + var platform = AvaloniaNativePlatform.Initialize( AvaloniaLocator.Current.GetService() ?? - new AvaloniaNativePlatformOptions())); + new AvaloniaNativePlatformOptions()); + + builder.AfterSetup (x=> + { + platform.SetupApplicationName(); + platform.SetupApplicationMenuExporter(); + }); + }); + return builder; } } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 490d5688a8..a7828bedaf 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -3,13 +3,14 @@ using System; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Native.Interop; using Avalonia.Platform; using Avalonia.Platform.Interop; namespace Avalonia.Native { - public class WindowImpl : WindowBaseImpl, IWindowImpl + public class WindowImpl : WindowBaseImpl, IWindowImpl, ITopLevelImplWithNativeMenuExporter { private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; @@ -22,6 +23,8 @@ namespace Avalonia.Native { Init(_native = factory.CreateWindow(e), factory.CreateScreens()); } + + NativeMenuExporter = new AvaloniaNativeMenuExporter(_native, factory); } class WindowEvents : WindowBaseEvents, IAvnWindowEvents @@ -104,6 +107,9 @@ namespace Avalonia.Native } public Func Closing { get; set; } + + public ITopLevelNativeMenuExporter NativeMenuExporter { get; } + public void Move(PixelPoint point) => Position = point; public override IPopupImpl CreatePopup() => diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 114979fba2..67279fca99 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -51,4 +51,5 @@ + diff --git a/src/Avalonia.Themes.Default/InverseBooleanValueConverter.cs b/src/Avalonia.Themes.Default/InverseBooleanValueConverter.cs new file mode 100644 index 0000000000..7befc81b8e --- /dev/null +++ b/src/Avalonia.Themes.Default/InverseBooleanValueConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Avalonia.Themes.Default +{ + class InverseBooleanValueConverter : IValueConverter + { + public bool Default { get; set; } + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is bool b ? !b : Default; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is bool b ? !b : !Default; + } + } +} diff --git a/src/Avalonia.Themes.Default/NativeMenuBar.xaml b/src/Avalonia.Themes.Default/NativeMenuBar.xaml new file mode 100644 index 0000000000..2832bab226 --- /dev/null +++ b/src/Avalonia.Themes.Default/NativeMenuBar.xaml @@ -0,0 +1,25 @@ + + + + + + diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 1d2290236c..d7a7bb97fd 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -38,7 +38,9 @@ namespace Avalonia.X11 throw new Exception("XOpenDisplay failed"); XError.Init(); Info = new X11Info(Display, DeferredDisplay); - + //TODO: log + if (options.UseDBusMenu) + DBusHelper.TryInitialize(); AvaloniaLocator.CurrentMutable.BindToSelf(this) .Bind().ToConstant(this) .Bind().ToConstant(new X11PlatformThreading(this)) @@ -95,6 +97,7 @@ namespace Avalonia public bool UseEGL { get; set; } public bool UseGpu { get; set; } = true; public bool OverlayPopups { get; set; } + public bool UseDBusMenu { get; set; } public List GlxRendererBlacklist { get; set; } = new List { diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 5b9eebb9cb..860456a838 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -6,7 +6,9 @@ using System.Linq; using System.Reactive.Disposables; using System.Text; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.FreeDesktop; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.OpenGL; @@ -19,7 +21,7 @@ using static Avalonia.X11.XLib; // ReSharper disable StringLiteralTypo namespace Avalonia.X11 { - unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client + unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client, ITopLevelImplWithNativeMenuExporter { private readonly AvaloniaX11Platform _platform; private readonly IWindowImpl _popupParent; @@ -170,6 +172,8 @@ namespace Avalonia.X11 XFlush(_x11.Display); if(_popup) PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize)); + if (platform.Options.UseDBusMenu) + NativeMenuExporter = DBusMenuExporter.TryCreate(_handle); } class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo @@ -975,5 +979,6 @@ namespace Avalonia.X11 } public IPopupPositioner PopupPositioner { get; } + public ITopLevelNativeMenuExporter NativeMenuExporter { get; } } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 8fc555aac2..db37e4af0b 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -118,8 +118,7 @@ public static class LinuxFramebufferPlatformExtensions where T : AppBuilderBase, new() { var lifetime = LinuxFramebufferPlatform.Initialize(builder, backend); - builder.Instance.ApplicationLifetime = lifetime; - builder.SetupWithoutStarting(); + builder.SetupWithLifetime(lifetime); lifetime.Start(args); builder.Instance.Run(lifetime.Token); return lifetime.ExitCode; diff --git a/src/iOS/Avalonia.iOS/AppBuilder.cs b/src/iOS/Avalonia.iOS/AppBuilder.cs index a68dd6387a..cb8e0a7954 100644 --- a/src/iOS/Avalonia.iOS/AppBuilder.cs +++ b/src/iOS/Avalonia.iOS/AppBuilder.cs @@ -6,7 +6,7 @@ namespace Avalonia public class AppBuilder : AppBuilderBase { public AppBuilder() : base(new StandardRuntimePlatform(), - builder => StandardRuntimePlatformServices.Register(builder.Instance?.GetType().Assembly)) + builder => StandardRuntimePlatformServices.Register(builder.ApplicationType.Assembly)) { }