瀏覽代碼

Merge branch 'exported-menus'

Dan Walmsley 6 年之前
父節點
當前提交
e80ddf5c65
共有 51 個文件被更改,包括 2191 次插入73 次删除
  1. 1 0
      Avalonia.sln.DotSettings
  2. 1 1
      global.json
  3. 31 0
      native/Avalonia.Native/inc/avalonia-native.h
  4. 9 0
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  5. 8 0
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  6. 12 0
      native/Avalonia.Native/src/OSX/common.h
  7. 147 4
      native/Avalonia.Native/src/OSX/main.mm
  8. 80 0
      native/Avalonia.Native/src/OSX/menu.h
  9. 305 0
      native/Avalonia.Native/src/OSX/menu.mm
  10. 0 6
      native/Avalonia.Native/src/OSX/platformthreading.mm
  11. 1 0
      native/Avalonia.Native/src/OSX/window.h
  12. 98 0
      native/Avalonia.Native/src/OSX/window.mm
  13. 1 1
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  14. 1 1
      samples/ControlCatalog.Android/MainActivity.cs
  15. 5 1
      samples/ControlCatalog.NetCore/Program.cs
  16. 15 3
      samples/ControlCatalog/App.xaml
  17. 13 0
      samples/ControlCatalog/App.xaml.cs
  18. 25 0
      samples/ControlCatalog/DecoratedWindow.xaml
  19. 30 1
      samples/ControlCatalog/MainWindow.xaml
  20. 14 0
      samples/ControlCatalog/MainWindow.xaml.cs
  21. 4 1
      samples/ControlCatalog/Pages/MenuPage.xaml
  22. 3 3
      src/Android/Avalonia.Android/AndroidPlatform.cs
  23. 1 1
      src/Android/Avalonia.Android/Avalonia.Android.csproj
  24. 1 1
      src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
  25. 29 21
      src/Avalonia.Controls/AppBuilderBase.cs
  26. 18 1
      src/Avalonia.Controls/Application.cs
  27. 1 2
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  28. 85 0
      src/Avalonia.Controls/NativeMenu.Export.cs
  29. 60 0
      src/Avalonia.Controls/NativeMenu.cs
  30. 36 0
      src/Avalonia.Controls/NativeMenuBar.cs
  31. 162 0
      src/Avalonia.Controls/NativeMenuItem.cs
  32. 23 0
      src/Avalonia.Controls/NativeMenuItemBase.cs
  33. 10 0
      src/Avalonia.Controls/NativeMenuItemSeperator.cs
  34. 18 0
      src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs
  35. 1 10
      src/Avalonia.DesktopRuntime/AppBuilder.cs
  36. 4 0
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  37. 82 0
      src/Avalonia.FreeDesktop/DBusHelper.cs
  38. 56 0
      src/Avalonia.FreeDesktop/DBusMenu.cs
  39. 387 0
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  40. 6 1
      src/Avalonia.Input/KeyGesture.cs
  41. 305 0
      src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
  42. 26 6
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  43. 11 2
      src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
  44. 7 1
      src/Avalonia.Native/WindowImpl.cs
  45. 1 0
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  46. 20 0
      src/Avalonia.Themes.Default/InverseBooleanValueConverter.cs
  47. 25 0
      src/Avalonia.Themes.Default/NativeMenuBar.xaml
  48. 4 1
      src/Avalonia.X11/X11Platform.cs
  49. 6 1
      src/Avalonia.X11/X11Window.cs
  50. 1 2
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  51. 1 1
      src/iOS/Avalonia.iOS/AppBuilder.cs

+ 1 - 0
Avalonia.sln.DotSettings

@@ -3,6 +3,7 @@
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=3E53A01A_002DB331_002D47F3_002DB828_002D4A5717E77A24_002Fd_003Aglass/@EntryIndexedValue">ExplicitlyExcluded</s:String>
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=6417B24E_002D49C2_002D4985_002D8DB2_002D3AB9D898EC91/@EntryIndexedValue">ExplicitlyExcluded</s:String>
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=E3A1060B_002D50D0_002D44E8_002D88B6_002DF44EF2E5BD72_002Ff_003Ahtml_002Ehtm/@EntryIndexedValue">ExplicitlyExcluded</s:String>
+	<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBePrivate_002EGlobal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
 	<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantUsingDirective/@EntryIndexedValue">HINT</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=DECLSPEC_005FPROPERTY/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=ENUM/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>

+ 1 - 1
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"
     }
 }

+ 31 - 0
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();

+ 9 - 0
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 = "<group>"; };
+		37155CE3233C00EB0034DCE9 /* menu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = menu.h; sourceTree = "<group>"; };
 		379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = "<group>"; };
 		37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = "<group>"; };
 		37A517B22159597E00FBA241 /* Screens.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = Screens.mm; sourceTree = "<group>"; };
@@ -32,6 +35,7 @@
 		37DDA9AF219330F8002E132B /* AvnString.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnString.mm; sourceTree = "<group>"; };
 		37DDA9B121933371002E132B /* AvnString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnString.h; sourceTree = "<group>"; };
 		37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = "<group>"; };
+		520624B222973F4100C4DCEF /* menu.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = menu.mm; sourceTree = "<group>"; };
 		5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = "<group>"; };
 		5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = "<group>"; };
 		5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = "<group>"; };
@@ -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 */,

+ 8 - 0
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>

+ 12 - 0
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<typename T> inline T* objc_cast(id from) {
     return nil;
 }
 
+@interface ActionCallback : NSObject
+- (ActionCallback*) initWithCallback: (IAvnActionCallback*) callback;
+- (void) action;
+@end
+
 #endif

+ 147 - 4
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<LSGetCurrentApplicationASNType>(
+                                                         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<LSSetApplicationInformationItemType>(
+                                                              CFBundleGetFunctionPointerForName(
+                                                                                                launch_services_bundle,
+                                                                                                CFSTR("_LSSetApplicationInformationItem")));
+        if (!ls_set_application_information_item_func){}
+        //LOG(ERROR) << "Could not find _LSSetApplicationInformationItem";
+        
+        CFStringRef* key_pointer = reinterpret_cast<CFStringRef*>(
+                                                                  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<IAvnMacOptions, &IID_IAvnMacOptions>
 {
 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()

+ 80 - 0
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<IAvnAppMenuItem, &IID_IAvnAppMenuItem>
+{
+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<IAvnAppMenu, &IID_IAvnAppMenu>
+{
+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
+

+ 305 - 0
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<AvnAppMenu*>(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<AvnAppMenuItem*>(item);
+    
+    if(avnMenuItem != nullptr)
+    {
+        [_native addItem: avnMenuItem->GetNative()];
+    }
+    
+    return S_OK;
+}
+
+HRESULT AvnAppMenu::RemoveItem (IAvnAppMenuItem* item)
+{
+    auto avnMenuItem = dynamic_cast<AvnAppMenuItem*>(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<AvnAppMenu*>(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;
+}
+
+

+ 0 - 6
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<IAvnActionCallback> _callback;

+ 1 - 0
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

+ 98 - 0
native/Avalonia.Native/src/OSX/window.mm

@@ -5,6 +5,7 @@
 #include "window.h"
 #include "KeyTransform.h"
 #include "cursor.h"
+#include "menu.h"
 #include <OpenGL/gl.h>
 
 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<AvnAppMenu*>(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<WindowBaseImpl> _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<AvnAppMenu*>(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];
 }
 

+ 1 - 1
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@@ -16,7 +16,7 @@
     <AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
     <GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
     <AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk>
-    <TargetFrameworkVersion>v8.0</TargetFrameworkVersion>
+    <TargetFrameworkVersion>v9.0</TargetFrameworkVersion>
     <AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

+ 1 - 1
samples/ControlCatalog.Android/MainActivity.cs

@@ -20,7 +20,7 @@ namespace ControlCatalog.Android
         {
             if (Avalonia.Application.Current == null)           
             {
-                AppBuilder.Configure(new App())
+                AppBuilder.Configure<App>()
                     .UseAndroid()
                     .SetupWithoutStarting();
                 Content = new MainView();

+ 5 - 1
samples/ControlCatalog.NetCore/Program.cs

@@ -55,7 +55,11 @@ namespace ControlCatalog.NetCore
         public static AppBuilder BuildAvaloniaApp()
             => AppBuilder.Configure<App>()
                 .UsePlatformDetect()
-                .With(new X11PlatformOptions { EnableMultiTouch = true })
+                .With(new X11PlatformOptions
+                {
+                    EnableMultiTouch = true,
+                    UseDBusMenu = true
+                })
                 .With(new Win32PlatformOptions
                 {
                     EnableMultitouch = true,

+ 15 - 3
samples/ControlCatalog/App.xaml

@@ -2,9 +2,9 @@
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              x:Class="ControlCatalog.App">
   <Application.Styles>
-      <StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
-      <StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
-      <StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Default.xaml"/>
+    <StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
+    <StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
+    <StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Default.xaml"/>
     <Style Selector="TextBlock.h1">
       <Setter Property="FontSize" Value="{DynamicResource FontSizeLarge}"/>
       <Setter Property="FontWeight" Value="Medium"/>
@@ -17,4 +17,16 @@
     </Style>
     <StyleInclude Source="/SideBar.xaml"/>
   </Application.Styles>
+
+  <NativeMenu.Menu>
+      <NativeMenu>
+        <NativeMenuItem Header="Open" Clicked="OnOpenClicked"/>
+        <NativeMenuItem Header="Recent">
+          <NativeMenuItem.Menu>
+            <NativeMenu/>
+          </NativeMenuItem.Menu>
+        </NativeMenuItem>
+        <NativeMenuItem Header="Quit Avalonia" Gesture="CMD+Q"/>
+      </NativeMenu>
+  </NativeMenu.Menu>
 </Application>

+ 13 - 0
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()

+ 25 - 0
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">
+        <NativeMenu.Menu>
+    <NativeMenu>
+      <NativeMenuItem Header="Decorated">
+        <NativeMenuItem.Menu>
+          <NativeMenu>
+            <NativeMenuItem Header="Open"/>
+            <NativeMenuItem Header="Recent">
+              <NativeMenuItem.Menu>
+                <NativeMenu/>
+              </NativeMenuItem.Menu>
+            </NativeMenuItem>
+            <NativeMenuItem Header="Quit Avalonia" Gesture="CMD+Q"/>
+          </NativeMenu>
+        </NativeMenuItem.Menu>
+      </NativeMenuItem>
+      <NativeMenuItem Header="Edit">
+        <NativeMenuItem.Menu>
+          <NativeMenu>
+            <NativeMenuItem Header="Copy"/>
+            <NativeMenuItem Header="Paste"/>
+          </NativeMenu>
+        </NativeMenuItem.Menu>
+      </NativeMenuItem>
+    </NativeMenu>
+  </NativeMenu.Menu>
     <Grid RowDefinitions="5,*,5" ColumnDefinitions="5,*,5">
         <DockPanel  Grid.Column="1"  Grid.Row="1" >
             <Grid Name="TitleBar" Background="LightBlue" DockPanel.Dock="Top" ColumnDefinitions="Auto,*,Auto">

+ 30 - 1
samples/ControlCatalog/MainWindow.xaml

@@ -8,7 +8,36 @@
         xmlns:vm="clr-namespace:ControlCatalog.ViewModels"
         xmlns:v="clr-namespace:ControlCatalog.Views"
         x:Class="ControlCatalog.MainWindow">
-    <Window.DataTemplates>
+
+  <NativeMenu.Menu>
+    <NativeMenu>
+      <NativeMenuItem Header="File">
+        <NativeMenuItem.Menu>
+          <NativeMenu>
+            <NativeMenuItem Header="Open" Clicked="OnOpenClicked"/>
+            <NativeMenuItemSeperator/>
+            <NativeMenuItem Header="Recent">
+              <NativeMenuItem.Menu>
+                <NativeMenu/>
+              </NativeMenuItem.Menu>
+            </NativeMenuItem>
+            <NativeMenuItemSeperator/>
+            <NativeMenuItem Header="Quit Avalonia" Clicked="OnCloseClicked" Gesture="CMD+Q"/>
+          </NativeMenu>
+        </NativeMenuItem.Menu>
+      </NativeMenuItem>
+      <NativeMenuItem Header="Edit">
+        <NativeMenuItem.Menu>
+          <NativeMenu>
+            <NativeMenuItem Header="Copy"/>
+            <NativeMenuItem Header="Paste"/>
+          </NativeMenu>
+        </NativeMenuItem.Menu>
+      </NativeMenuItem>
+    </NativeMenu>
+  </NativeMenu.Menu>
+
+  <Window.DataTemplates>
         <DataTemplate DataType="vm:NotificationViewModel">
             <v:CustomNotificationView />
         </DataTemplate>

+ 14 - 0
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

+ 4 - 1
samples/ControlCatalog/Pages/MenuPage.xaml

@@ -3,8 +3,11 @@
              x:Class="ControlCatalog.Pages.MenuPage">
   <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">Menu</TextBlock>
+    <TextBlock Classes="h2">Exported menu fallback</TextBlock>
+    <TextBlock>(Should be only visible on platforms without desktop-global menu bar)</TextBlock>
+    <NativeMenuBar/>
     <TextBlock Classes="h2">A window menu</TextBlock>
-
+            
         <StackPanel Orientation="Horizontal"
               Margin="0,16,0,0"
               HorizontalAlignment="Center"

+ 3 - 3
src/Android/Avalonia.Android/AndroidPlatform.cs

@@ -17,7 +17,7 @@ namespace Avalonia
     {
         public static T UseAndroid<T>(this T builder) where T : AppBuilderBase<T>, 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<IClipboard>().ToTransient<ClipboardImpl>()
@@ -55,7 +55,7 @@ namespace Avalonia.Android
                 .Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop())
                 .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
-                .Bind<IAssetLoader>().ToConstant(new AssetLoader(app.GetType().Assembly));
+                .Bind<IAssetLoader>().ToConstant(new AssetLoader(appType.Assembly));
 
             SkiaPlatform.Initialize();
             ((global::Android.App.Application) global::Android.App.Application.Context.ApplicationContext)

+ 1 - 1
src/Android/Avalonia.Android/Avalonia.Android.csproj

@@ -1,6 +1,6 @@
 <Project Sdk="MSBuild.Sdk.Extras">
   <PropertyGroup>
-    <TargetFramework>monoandroid80</TargetFramework>
+    <TargetFramework>monoandroid90</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
   <ItemGroup>

+ 1 - 1
src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj

@@ -16,7 +16,7 @@
     <AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
     <GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
     <AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk>
-    <TargetFrameworkVersion>v8.0</TargetFrameworkVersion>
+    <TargetFrameworkVersion>v9.0</TargetFrameworkVersion>
     <AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

+ 29 - 21
src/Avalonia.Controls/AppBuilderBase.cs

@@ -18,7 +18,9 @@ namespace Avalonia.Controls
     {
         private static bool s_setupWasAlreadyCalled;
         private Action _optionsInitializers;
-
+        private Func<Application> _appFactory;
+        private IApplicationLifetime _lifetime;
+        
         /// <summary>
         /// Gets or sets the <see cref="IRuntimePlatform"/> instance.
         /// </summary>
@@ -30,10 +32,15 @@ namespace Avalonia.Controls
         public Action RuntimePlatformServicesInitializer { get; private set; }
 
         /// <summary>
-        /// Gets or sets the <see cref="Application"/> instance being initialized.
+        /// Gets the <see cref="Application"/> instance being initialized.
         /// </summary>
-        public Application Instance { get; protected set; }
-
+        public Application Instance { get; private set; }
+        
+        /// <summary>
+        /// Gets the type of the Instance (even if it's not created yet)
+        /// </summary>
+        public Type ApplicationType { get; private set; }
+        
         /// <summary>
         /// Gets or sets a method to call the initialize the windowing subsystem.
         /// </summary>
@@ -76,20 +83,11 @@ namespace Avalonia.Controls
         public static TAppBuilder Configure<TApp>()
             where TApp : Application, new()
         {
-            return Configure(new TApp());
-        }
-
-        /// <summary>
-        /// Begin configuring an <see cref="Application"/>.
-        /// </summary>
-        /// <returns>An <typeparamref name="TAppBuilder"/> instance.</returns>
-        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;
         }
 
+        /// <summary>
+        /// Sets up the platform-specific services for the application and initialized it with a particular lifetime, but does not run it.
+        /// </summary>
+        /// <param name="lifetime"></param>
+        /// <returns></returns>
+        public TAppBuilder SetupWithLifetime(IApplicationLifetime lifetime)
+        {
+            _lifetime = lifetime;
+            Setup();
+            return Self;
+        }
+        
         /// <summary>
         /// Specifies a windowing subsystem to use.
         /// </summary>
@@ -254,11 +264,6 @@ namespace Avalonia.Controls
         /// </summary>
         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);

+ 18 - 1
src/Avalonia.Controls/Application.cs

@@ -32,7 +32,7 @@ namespace Avalonia
     /// method.
     /// - Tracks the lifetime of the application.
     /// </remarks>
-    public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode
+    public class Application : AvaloniaObject, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode
     {
         /// <summary>
         /// The application-global data templates.
@@ -210,5 +210,22 @@ namespace Avalonia
         {
             ResourcesChanged?.Invoke(this, e);
         }
+
+        private string _name;
+        /// <summary>
+        /// Defines Name property
+        /// </summary>
+        public static readonly DirectProperty<Application, string> NameProperty =
+            AvaloniaProperty.RegisterDirect<Application, string>("Name", o => o.Name, (o, v) => o.Name = v);
+
+        /// <summary>
+        /// Application name to be used for various platform-specific purposes
+        /// </summary>
+        public string Name
+        {
+            get => _name;
+            set => SetAndRaise(NameProperty, ref _name, value);
+        }
+
     }
 }

+ 1 - 2
src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

@@ -125,8 +125,7 @@ namespace Avalonia
             where T : AppBuilderBase<T>, new()
         {
             var lifetime = new ClassicDesktopStyleApplicationLifetime(builder.Instance) {ShutdownMode = shutdownMode};
-            builder.Instance.ApplicationLifetime = lifetime;
-            builder.SetupWithoutStarting();
+            builder.SetupWithLifetime(lifetime);
             return lifetime.Start(args);
         }
     }

+ 85 - 0
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<bool> IsNativeMenuExportedProperty =
+            AvaloniaProperty.RegisterAttached<NativeMenu, TopLevel, bool>("IsNativeMenuExported");
+
+        public static bool GetIsNativeMenuExported(TopLevel tl) => tl.GetValue(IsNativeMenuExportedProperty);
+        
+        private static readonly AttachedProperty<NativeMenuInfo> s_nativeMenuInfoProperty =
+            AvaloniaProperty.RegisterAttached<NativeMenu, TopLevel, NativeMenuInfo>("___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<NativeMenu> MenuProperty
+            = AvaloniaProperty.RegisterAttached<NativeMenu, AvaloniaObject, NativeMenu>("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);
+                }
+            });
+        }
+    }
+}

+ 60 - 0
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<NativeMenuItemBase>
+    {
+        private readonly AvaloniaList<NativeMenuItemBase> _items =
+            new AvaloniaList<NativeMenuItemBase> { ResetBehavior = ResetBehavior.Remove };
+        private NativeMenuItem _parent;
+        [Content]
+        public IList<NativeMenuItemBase> 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<NativeMenu, NativeMenuItem> ParentProperty =
+            AvaloniaProperty.RegisterDirect<NativeMenu, NativeMenuItem>("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<NativeMenuItemBase> GetEnumerator() => _items.GetEnumerator();
+
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return GetEnumerator();
+        }
+    }
+}

+ 36 - 0
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<bool> EnableMenuItemClickForwardingProperty =
+            AvaloniaProperty.RegisterAttached<NativeMenuBar, MenuItem, Boolean>(
+                "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();
+        }
+    }
+}

+ 162 - 0
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<EventArgs>
+        {
+            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<NativeMenuItem, NativeMenu> MenuProperty =
+            AvaloniaProperty.RegisterDirect<NativeMenuItem, NativeMenu>(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<NativeMenuItem, string> HeaderProperty =
+            AvaloniaProperty.RegisterDirect<NativeMenuItem, string>(nameof(Header), o => o._header, (o, v) => o._header = v);
+
+        public string Header
+        {
+            get => GetValue(HeaderProperty);
+            set => SetValue(HeaderProperty, value);
+        }
+
+        public static readonly DirectProperty<NativeMenuItem, KeyGesture> GestureProperty =
+            AvaloniaProperty.RegisterDirect<NativeMenuItem, KeyGesture>(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<NativeMenuItem, ICommand> CommandProperty =
+           AvaloniaProperty.RegisterDirect<NativeMenuItem, ICommand>(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();
+               });
+
+        /// <summary>
+        /// Defines the <see cref="CommandParameter"/> property.
+        /// </summary>
+        public static readonly StyledProperty<object> CommandParameterProperty =
+            Button.CommandParameterProperty.AddOwner<MenuItem>();
+
+        public static readonly DirectProperty<NativeMenuItem, bool> EnabledProperty =
+           AvaloniaProperty.RegisterDirect<NativeMenuItem, bool>(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);
+        }
+
+        /// <summary>
+        /// Gets or sets the parameter to pass to the <see cref="Command"/> property of a
+        /// <see cref="NativeMenuItem"/>.
+        /// </summary>
+        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);
+            }
+        }
+    }
+}

+ 23 - 0
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<NativeMenuItem, NativeMenu> ParentProperty =
+            AvaloniaProperty.RegisterDirect<NativeMenuItem, NativeMenu>("Parent", o => o.Parent, (o, v) => o.Parent = v);
+
+        public NativeMenu Parent
+        {
+            get => _parent;
+            set => SetAndRaise(ParentProperty, ref _parent, value);
+        }
+    }
+}

+ 10 - 0
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 => "-";
+    }
+}

+ 18 - 0
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; }
+    }
+}

+ 1 - 10
src/Avalonia.DesktopRuntime/AppBuilder.cs

@@ -18,19 +18,10 @@ namespace Avalonia
         /// </summary>
         public AppBuilder()
             : base(new StandardRuntimePlatform(),
-                builder => StandardRuntimePlatformServices.Register(builder.Instance?.GetType()?.Assembly))
+                builder => StandardRuntimePlatformServices.Register(builder.ApplicationType.Assembly))
         {
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="AppBuilder"/> class.
-        /// </summary>
-        /// <param name="app">The <see cref="Application"/> instance.</param>
-        public AppBuilder(Application app) : this()
-        {
-            Instance = app;
-        }
-
         bool CheckEnvironment(Type checkerType)
         {
             if (checkerType == null)

+ 4 - 0
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

@@ -8,4 +8,8 @@
     <ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
   </ItemGroup>
 
+  <ItemGroup>
+    <PackageReference Include="Tmds.DBus" Version="0.7.0" />
+  </ItemGroup>
+
 </Project>

+ 82 - 0
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
+    {
+        /// <summary>
+        /// This class uses synchronous execution at DBus connection establishment stage
+        /// then switches to using AvaloniaSynchronizationContext
+        /// </summary>
+        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;
+        }
+    }
+}

+ 56 - 0
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<object> GetAsync(string prop);
+        Task<DBusMenuProperties> GetAllAsync();
+        Task SetAsync(string prop, object val);
+        Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler);
+    }
+
+    [DBusInterface("com.canonical.dbusmenu")]
+    interface IDBusMenu : IFreeDesktopDBusProperties
+    {
+        Task<(uint revision, (int, KeyValuePair<string, object>[], object[]) layout)> GetLayoutAsync(int ParentId, int RecursionDepth, string[] PropertyNames);
+        Task<(int, KeyValuePair<string, object>[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames);
+        Task<object> GetPropertyAsync(int Id, string Name);
+        Task EventAsync(int Id, string EventId, object Data, uint Timestamp);
+        Task<int[]> EventGroupAsync((int id, string eventId, object data, uint timestamp)[] events);
+        Task<bool> AboutToShowAsync(int Id);
+        Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids);
+        Task<IDisposable> WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action<Exception> 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<IDisposable> WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchWindowUnregisteredAsync(Action<uint> handler, Action<Exception> onError = null);
+    }
+}

+ 387 - 0
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<int, NativeMenuItemBase> _idsToItems = new Dictionary<int, NativeMenuItemBase>();
+            private Dictionary<NativeMenuItemBase, int> _itemsToIds = new Dictionary<NativeMenuItemBase, int>();
+            private readonly HashSet<NativeMenu> _menus = new HashSet<NativeMenu>();
+            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<IRegistrar>(
+                        "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<object> IFreeDesktopDBusProperties.GetAsync(string prop)
+            {
+                if (prop == "Version")
+                    return 2;
+                if (prop == "Status")
+                    return "normal";
+                return 0;
+            }
+
+            async Task<DBusMenuProperties> 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 ?? "<null>";
+                    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<string>();
+                        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<KeyValuePair<string, object>> _reusablePropertyList = new List<KeyValuePair<string, object>>();
+            KeyValuePair<string, object>[] 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<string, object>(n, v));
+                }
+
+                return _reusablePropertyList.ToArray();
+            }
+
+            
+            public Task SetAsync(string prop, object val) => Task.CompletedTask;
+
+            public Task<(uint revision, (int, KeyValuePair<string, object>[], 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<string, object>[], 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<string, object>[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames)
+            {
+                var arr = new (int, KeyValuePair<string, object>[])[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<object> 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<int[]> 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<bool> 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<string, object>)[] updatedProps, (int, string[])[] removedProps)>
+                ItemsPropertiesUpdated;
+            private event Action<(uint revision, int parent)> LayoutUpdated;
+            private event Action<(int id, uint timestamp)> ItemActivationRequested;
+            private event Action<PropertyChanges> PropertiesChanged;
+
+            async Task<IDisposable> IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> handler, Action<Exception> onError)
+            {
+                ItemsPropertiesUpdated += handler;
+                return Disposable.Create(() => ItemsPropertiesUpdated -= handler);
+            }
+            async Task<IDisposable> IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action<Exception> onError)
+            {
+                LayoutUpdated += handler;
+                return Disposable.Create(() => LayoutUpdated -= handler);
+            }
+
+            async Task<IDisposable> IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action<Exception> onError)
+            {
+                ItemActivationRequested+= handler;
+                return Disposable.Create(() => ItemActivationRequested -= handler);
+            }
+
+            async Task<IDisposable> IFreeDesktopDBusProperties.WatchPropertiesAsync(Action<PropertyChanges> handler)
+            {
+                PropertiesChanged += handler;
+                return Disposable.Create(() => PropertiesChanged -= handler);
+            }
+
+            #endregion
+        }
+    }
+}

+ 6 - 1
src/Avalonia.Input/KeyGesture.cs

@@ -14,7 +14,7 @@ namespace Avalonia.Input
     {
         private static readonly Dictionary<string, Key> s_keySynonyms = new Dictionary<string, Key>
         {
-            { "+", 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);
         }
 

+ 305 - 0
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<bool> _predicate;
+
+        public PredicateCallback(Func<bool> 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<NativeMenuItem> _menuItems = new List<NativeMenuItem>();
+
+        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<NativeMenuItemBase> 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<NativeMenuItemBase> 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<NativeMenuItemBase> 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<NativeMenuItemBase> { menuItem });
+
+            _factory.SetAppMenu(appMenu);
+        }
+
+        private void SetMenu(IAvnWindow avnWindow, ICollection<NativeMenuItemBase> menuItems)
+        {
+            if (menuItems is null)
+            {
+                menuItems = new List<NativeMenuItemBase>();
+            }
+
+            var appMenu = avnWindow.ObtainMainMenu();
+
+            if (appMenu is null)
+            {
+                appMenu = _factory.CreateMenu();
+            }
+
+            appMenu.Clear();
+            AddItemsToMenu(appMenu, menuItems);
+
+            avnWindow.SetMainMenu(appMenu);
+        }
+    }
+}

+ 26 - 6
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<CreateAvaloniaNativeDelegate>(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<MacOSPlatformOptions>();
+
                 _factory.MacOptions.ShowInDock = macOpts?.ShowInDock != false ? 1 : 0;
             }
 

+ 11 - 2
src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs

@@ -13,9 +13,18 @@ namespace Avalonia
             where T : AppBuilderBase<T>, new()
         {
             builder.UseWindowingSubsystem(() =>
-                AvaloniaNativePlatform.Initialize(
+            {
+                var platform = AvaloniaNativePlatform.Initialize(
                     AvaloniaLocator.Current.GetService<AvaloniaNativePlatformOptions>() ??
-                    new AvaloniaNativePlatformOptions()));
+                    new AvaloniaNativePlatformOptions());
+
+                    builder.AfterSetup (x=>
+                    {
+                        platform.SetupApplicationName();
+                        platform.SetupApplicationMenuExporter();
+                    });
+            });
+
             return builder;
         }
     }

+ 7 - 1
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<bool> Closing { get; set; }
+
+        public ITopLevelNativeMenuExporter NativeMenuExporter { get; }
+
         public void Move(PixelPoint point) => Position = point;
 
         public override IPopupImpl CreatePopup() =>

+ 1 - 0
src/Avalonia.Themes.Default/DefaultTheme.xaml

@@ -51,4 +51,5 @@
   <StyleInclude Source="resm:Avalonia.Themes.Default.AutoCompleteBox.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.WindowNotificationManager.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.NotificationCard.xaml?assembly=Avalonia.Themes.Default"/>
+  <StyleInclude Source="resm:Avalonia.Themes.Default.NativeMenuBar.xaml?assembly=Avalonia.Themes.Default"/>
 </Styles>

+ 20 - 0
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;
+        }
+    }
+}

+ 25 - 0
src/Avalonia.Themes.Default/NativeMenuBar.xaml

@@ -0,0 +1,25 @@
+<Style xmlns="https://github.com/avaloniaui"
+       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+       xmlns:local="clr-namespace:Avalonia.Themes.Default"
+       Selector="NativeMenuBar">
+  <Style.Resources>
+    <local:InverseBooleanValueConverter x:Key="AvaloniaThemesDefaultNativeMenuBarInverseBooleanValueConverter" Default="True"/>
+  </Style.Resources>
+  <Setter Property="Template">
+    <ControlTemplate>
+      <Menu
+        IsVisible="{Binding $parent[TopLevel].(NativeMenu.IsNativeMenuExported), Converter={StaticResource AvaloniaThemesDefaultNativeMenuBarInverseBooleanValueConverter}}"
+        Items="{Binding $parent[TopLevel].(NativeMenu.Menu).Items}">
+        <Menu.Styles>
+          <Style Selector="MenuItem">
+            <Setter Property="Header" Value="{Binding Header}"/>
+            <Setter Property="Items" Value="{Binding Menu.Items}"/>
+            <Setter Property="Command" Value="{Binding Command}"/>
+            <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
+            <Setter Property="(NativeMenuBar.EnableMenuItemClickForwarding)" Value="True"/>
+          </Style>
+        </Menu.Styles>
+      </Menu>
+    </ControlTemplate>
+  </Setter>
+</Style>

+ 4 - 1
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<IWindowingPlatform>().ToConstant(this)
                 .Bind<IPlatformThreadingInterface>().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<string> GlxRendererBlacklist { get; set; } = new List<string>
         {

+ 6 - 1
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; }
     }
 }

+ 1 - 2
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@@ -118,8 +118,7 @@ public static class LinuxFramebufferPlatformExtensions
         where T : AppBuilderBase<T>, 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;

+ 1 - 1
src/iOS/Avalonia.iOS/AppBuilder.cs

@@ -6,7 +6,7 @@ namespace Avalonia
     public class AppBuilder : AppBuilderBase<AppBuilder>
     {
         public AppBuilder() : base(new StandardRuntimePlatform(),
-            builder => StandardRuntimePlatformServices.Register(builder.Instance?.GetType().Assembly))
+            builder => StandardRuntimePlatformServices.Register(builder.ApplicationType.Assembly))
         {
 
         }