diff --git a/Avalonia.sln.DotSettings b/Avalonia.sln.DotSettings
index 1361172fff..7060f4a62a 100644
--- a/Avalonia.sln.DotSettings
+++ b/Avalonia.sln.DotSettings
@@ -3,6 +3,7 @@
ExplicitlyExcluded
ExplicitlyExcluded
ExplicitlyExcluded
+ DO_NOT_SHOW
HINT
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
<Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" />
diff --git a/global.json b/global.json
index 6d12c28846..1e599211d4 100644
--- a/global.json
+++ b/global.json
@@ -1,7 +1,7 @@
{
"msbuild-sdks": {
"Microsoft.Build.Traversal": "1.0.43",
- "MSBuild.Sdk.Extras": "1.6.65",
+ "MSBuild.Sdk.Extras": "2.0.46",
"AggregatePackage.NuGet.Sdk" : "0.1.12"
}
}
diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h
index e54f3fa6a7..f1c7664c3e 100644
--- a/native/Avalonia.Native/inc/avalonia-native.h
+++ b/native/Avalonia.Native/inc/avalonia-native.h
@@ -22,6 +22,8 @@ struct IAvnGlContext;
struct IAvnGlDisplay;
struct IAvnGlSurfaceRenderTarget;
struct IAvnGlSurfaceRenderingSession;
+struct IAvnAppMenu;
+struct IAvnAppMenuItem;
struct AvnSize
{
@@ -173,6 +175,11 @@ public:
virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0;
virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0;
virtual HRESULT ObtainGlFeature(IAvnGlFeature** ppv) = 0;
+ virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) = 0;
+ virtual HRESULT SetAppMenu(IAvnAppMenu* menu) = 0;
+ virtual HRESULT CreateMenu (IAvnAppMenu** ppv) = 0;
+ virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) = 0;
+ virtual HRESULT CreateMenuItemSeperator (IAvnAppMenuItem** ppv) = 0;
};
AVNCOM(IAvnString, 17) : IUnknown
@@ -203,6 +210,8 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown
virtual HRESULT SetCursor(IAvnCursor* cursor) = 0;
virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ret) = 0;
virtual HRESULT GetSoftwareFramebuffer(AvnFramebuffer*ret) = 0;
+ virtual HRESULT SetMainMenu(IAvnAppMenu* menu) = 0;
+ virtual HRESULT ObtainMainMenu(IAvnAppMenu** retOut) = 0;
virtual bool TryLock() = 0;
virtual void Unlock() = 0;
};
@@ -258,6 +267,7 @@ AVNCOM(IAvnWindowEvents, 06) : IAvnWindowBaseEvents
AVNCOM(IAvnMacOptions, 07) : IUnknown
{
virtual HRESULT SetShowInDock(int show) = 0;
+ virtual HRESULT SetApplicationTitle (void* utf8string) = 0;
};
AVNCOM(IAvnActionCallback, 08) : IUnknown
@@ -367,4 +377,25 @@ AVNCOM(IAvnGlSurfaceRenderingSession, 16) : IUnknown
virtual HRESULT GetScaling(double* ret) = 0;
};
+AVNCOM(IAvnAppMenu, 17) : IUnknown
+{
+ virtual HRESULT AddItem (IAvnAppMenuItem* item) = 0;
+ virtual HRESULT RemoveItem (IAvnAppMenuItem* item) = 0;
+ virtual HRESULT SetTitle (void* utf8String) = 0;
+ virtual HRESULT Clear () = 0;
+};
+
+AVNCOM(IAvnPredicateCallback, 18) : IUnknown
+{
+ virtual bool Evaluate() = 0;
+};
+
+AVNCOM(IAvnAppMenuItem, 19) : IUnknown
+{
+ virtual HRESULT SetSubMenu (IAvnAppMenu* menu) = 0;
+ virtual HRESULT SetTitle (void* utf8String) = 0;
+ virtual HRESULT SetGesture (void* utf8String, AvnInputModifiers modifiers) = 0;
+ virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) = 0;
+};
+
extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative();
diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
index 1870ef7ab3..c0a49382a7 100644
--- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
+++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
@@ -8,10 +8,12 @@
/* Begin PBXBuildFile section */
1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; };
+ 37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; };
37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; };
37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; };
37DDA9B0219330F8002E132B /* AvnString.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37DDA9AF219330F8002E132B /* AvnString.mm */; };
37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37E2330E21583241000CB7E2 /* KeyTransform.mm */; };
+ 520624B322973F4100C4DCEF /* menu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 520624B222973F4100C4DCEF /* menu.mm */; };
5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; };
5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; };
AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; };
@@ -24,6 +26,7 @@
/* Begin PBXFileReference section */
1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = ""; };
+ 37155CE3233C00EB0034DCE9 /* menu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = menu.h; sourceTree = ""; };
379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = ""; };
37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = ""; };
37A517B22159597E00FBA241 /* Screens.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = Screens.mm; sourceTree = ""; };
@@ -32,6 +35,7 @@
37DDA9AF219330F8002E132B /* AvnString.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnString.mm; sourceTree = ""; };
37DDA9B121933371002E132B /* AvnString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnString.h; sourceTree = ""; };
37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = ""; };
+ 520624B222973F4100C4DCEF /* menu.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = menu.mm; sourceTree = ""; };
5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = ""; };
5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = ""; };
5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = ""; };
@@ -85,6 +89,8 @@
AB661C1F2148286E00291242 /* window.mm */,
37C09D8A21581EF2006A6758 /* window.h */,
AB00E4F62147CA920032A60A /* main.mm */,
+ 37155CE3233C00EB0034DCE9 /* menu.h */,
+ 520624B222973F4100C4DCEF /* menu.mm */,
37A517B22159597E00FBA241 /* Screens.mm */,
37C09D8721580FE4006A6758 /* SystemDialogs.mm */,
AB7A61F02147C815003C5833 /* Products */,
@@ -107,6 +113,7 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
+ 37155CE4233C00EB0034DCE9 /* menu.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -150,6 +157,7 @@
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
+ English,
en,
);
mainGroup = AB7A61E62147C814003C5833;
@@ -173,6 +181,7 @@
37DDA9B0219330F8002E132B /* AvnString.mm in Sources */,
AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */,
37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */,
+ 520624B322973F4100C4DCEF /* menu.mm in Sources */,
37A517B32159597E00FBA241 /* Screens.mm in Sources */,
AB00E4F72147CA920032A60A /* main.mm in Sources */,
37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */,
diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000000..18d981003d
--- /dev/null
+++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h
index 45ec40c361..10534dea26 100644
--- a/native/Avalonia.Native/src/OSX/common.h
+++ b/native/Avalonia.Native/src/OSX/common.h
@@ -19,6 +19,13 @@ extern IAvnClipboard* CreateClipboard();
extern IAvnCursorFactory* CreateCursorFactory();
extern IAvnGlFeature* GetGlFeature();
extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* view);
+extern IAvnAppMenu* CreateAppMenu();
+extern IAvnAppMenuItem* CreateAppMenuItem();
+extern IAvnAppMenuItem* CreateAppMenuItemSeperator();
+extern void SetAppMenu (NSString* appName, IAvnAppMenu* appMenu);
+extern IAvnAppMenu* GetAppMenu ();
+extern NSMenuItem* GetAppMenuItem ();
+
extern void InitializeAvnApp();
extern NSApplicationActivationPolicy AvnDesiredActivationPolicy;
extern NSPoint ToNSPoint (AvnPoint p);
@@ -40,4 +47,9 @@ template inline T* objc_cast(id from) {
return nil;
}
+@interface ActionCallback : NSObject
+- (ActionCallback*) initWithCallback: (IAvnActionCallback*) callback;
+- (void) action;
+@end
+
#endif
diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm
index 70bd1e67f6..9418782fd1 100644
--- a/native/Avalonia.Native/src/OSX/main.mm
+++ b/native/Avalonia.Native/src/OSX/main.mm
@@ -5,10 +5,121 @@
#define COM_GUIDS_MATERIALIZE
#include "common.h"
+static NSString* s_appTitle = @"Avalonia";
+
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+void SetProcessName(NSString* appTitle) {
+ s_appTitle = appTitle;
+
+ CFStringRef process_name = (__bridge CFStringRef)appTitle;
+
+ if (!process_name || CFStringGetLength(process_name) == 0) {
+ //NOTREACHED() << "SetProcessName given bad name.";
+ return;
+ }
+
+ if (![NSThread isMainThread]) {
+ //NOTREACHED() << "Should only set process name from main thread.";
+ return;
+ }
+
+ // Warning: here be dragons! This is SPI reverse-engineered from WebKit's
+ // plugin host, and could break at any time (although realistically it's only
+ // likely to break in a new major release).
+ // When 10.7 is available, check that this still works, and update this
+ // comment for 10.8.
+
+ // Private CFType used in these LaunchServices calls.
+ typedef CFTypeRef PrivateLSASN;
+ typedef PrivateLSASN (*LSGetCurrentApplicationASNType)();
+ typedef OSStatus (*LSSetApplicationInformationItemType)(int, PrivateLSASN,
+ CFStringRef,
+ CFStringRef,
+ CFDictionaryRef*);
+
+ static LSGetCurrentApplicationASNType ls_get_current_application_asn_func =
+ NULL;
+ static LSSetApplicationInformationItemType
+ ls_set_application_information_item_func = NULL;
+ static CFStringRef ls_display_name_key = NULL;
+
+ static bool did_symbol_lookup = false;
+ if (!did_symbol_lookup) {
+ did_symbol_lookup = true;
+ CFBundleRef launch_services_bundle =
+ CFBundleGetBundleWithIdentifier(CFSTR("com.apple.LaunchServices"));
+ if (!launch_services_bundle) {
+ //LOG(ERROR) << "Failed to look up LaunchServices bundle";
+ return;
+ }
+
+ ls_get_current_application_asn_func =
+ reinterpret_cast(
+ CFBundleGetFunctionPointerForName(
+ launch_services_bundle, CFSTR("_LSGetCurrentApplicationASN")));
+ if (!ls_get_current_application_asn_func){}
+ //LOG(ERROR) << "Could not find _LSGetCurrentApplicationASN";
+
+ ls_set_application_information_item_func =
+ reinterpret_cast(
+ CFBundleGetFunctionPointerForName(
+ launch_services_bundle,
+ CFSTR("_LSSetApplicationInformationItem")));
+ if (!ls_set_application_information_item_func){}
+ //LOG(ERROR) << "Could not find _LSSetApplicationInformationItem";
+
+ CFStringRef* key_pointer = reinterpret_cast(
+ CFBundleGetDataPointerForName(launch_services_bundle,
+ CFSTR("_kLSDisplayNameKey")));
+ ls_display_name_key = key_pointer ? *key_pointer : NULL;
+ if (!ls_display_name_key){}
+ //LOG(ERROR) << "Could not find _kLSDisplayNameKey";
+
+ // Internally, this call relies on the Mach ports that are started up by the
+ // Carbon Process Manager. In debug builds this usually happens due to how
+ // the logging layers are started up; but in release, it isn't started in as
+ // much of a defined order. So if the symbols had to be loaded, go ahead
+ // and force a call to make sure the manager has been initialized and hence
+ // the ports are opened.
+ ProcessSerialNumber psn;
+ GetCurrentProcess(&psn);
+ }
+ if (!ls_get_current_application_asn_func ||
+ !ls_set_application_information_item_func ||
+ !ls_display_name_key) {
+ return;
+ }
+
+ PrivateLSASN asn = ls_get_current_application_asn_func();
+ // Constant used by WebKit; what exactly it means is unknown.
+ const int magic_session_constant = -2;
+ OSErr err =
+ ls_set_application_information_item_func(magic_session_constant, asn,
+ ls_display_name_key,
+ process_name,
+ NULL /* optional out param */);
+ //LOG_IF(ERROR, err) << "Call to set process name failed, err " << err;
+}
+
class MacOptions : public ComSingleObject
{
public:
FORWARD_IUNKNOWN()
+
+ virtual HRESULT SetApplicationTitle(void* utf8String) override
+ {
+ auto appTitle = [NSString stringWithUTF8String:(const char*)utf8String];
+
+ [[NSProcessInfo processInfo] setProcessName:appTitle];
+
+
+ SetProcessName(appTitle);
+
+ return S_OK;
+ }
+
virtual HRESULT SetShowInDock(int show) override
{
AvnDesiredActivationPolicy = show
@@ -17,8 +128,6 @@ public:
}
};
-
-
/// See "Using POSIX Threads in a Cocoa Application" section here:
/// https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html#//apple_ref/doc/uid/20000738-125024
@interface ThreadingInitializer : NSObject
@@ -43,8 +152,6 @@ public:
close(_fds[0]);
close(_fds[1]);
}
-
-
@end
@@ -123,6 +230,42 @@ public:
*ppv = rv;
return S_OK;
}
+
+ virtual HRESULT CreateMenu (IAvnAppMenu** ppv) override
+ {
+ *ppv = ::CreateAppMenu();
+ return S_OK;
+ }
+
+ virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) override
+ {
+ *ppv = ::CreateAppMenuItem();
+ return S_OK;
+ }
+
+ virtual HRESULT CreateMenuItemSeperator (IAvnAppMenuItem** ppv) override
+ {
+ *ppv = ::CreateAppMenuItemSeperator();
+ return S_OK;
+ }
+
+ virtual HRESULT SetAppMenu (IAvnAppMenu* appMenu) override
+ {
+ ::SetAppMenu(s_appTitle, appMenu);
+ return S_OK;
+ }
+
+ virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) override
+ {
+ if(retOut == nullptr)
+ {
+ return E_POINTER;
+ }
+
+ *retOut = ::GetAppMenu();
+
+ return S_OK;
+ }
};
extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative()
diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h
new file mode 100644
index 0000000000..befbe6a7e0
--- /dev/null
+++ b/native/Avalonia.Native/src/OSX/menu.h
@@ -0,0 +1,80 @@
+//
+// menu.h
+// Avalonia.Native.OSX
+//
+// Created by Dan Walmsley on 01/08/2019.
+// Copyright © 2019 Avalonia. All rights reserved.
+//
+
+#ifndef menu_h
+#define menu_h
+
+#include "common.h"
+
+class AvnAppMenuItem;
+class AvnAppMenu;
+
+@interface AvnMenu : NSMenu // for some reason it doesnt detect nsmenu here but compiler doesnt complain
+- (void)setMenu:(NSMenu*) menu;
+@end
+
+@interface AvnMenuItem : NSMenuItem
+- (id) initWithAvnAppMenuItem: (AvnAppMenuItem*)menuItem;
+- (void)didSelectItem:(id)sender;
+@end
+
+class AvnAppMenuItem : public ComSingleObject
+{
+private:
+ NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem
+ IAvnActionCallback* _callback;
+ IAvnPredicateCallback* _predicate;
+ bool _isSeperator;
+
+public:
+ FORWARD_IUNKNOWN()
+
+ AvnAppMenuItem(bool isSeperator);
+
+ NSMenuItem* GetNative();
+
+ virtual HRESULT SetSubMenu (IAvnAppMenu* menu) override;
+
+ virtual HRESULT SetTitle (void* utf8String) override;
+
+ virtual HRESULT SetGesture (void* key, AvnInputModifiers modifiers) override;
+
+ virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) override;
+
+ bool EvaluateItemEnabled();
+
+ void RaiseOnClicked();
+};
+
+
+class AvnAppMenu : public ComSingleObject
+{
+private:
+ AvnMenu* _native;
+
+public:
+ FORWARD_IUNKNOWN()
+
+ AvnAppMenu();
+
+ AvnAppMenu(AvnMenu* native);
+
+ AvnMenu* GetNative();
+
+ virtual HRESULT AddItem (IAvnAppMenuItem* item) override;
+
+ virtual HRESULT RemoveItem (IAvnAppMenuItem* item) override;
+
+ virtual HRESULT SetTitle (void* utf8String) override;
+
+ virtual HRESULT Clear () override;
+};
+
+
+#endif
+
diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm
new file mode 100644
index 0000000000..d9dfe36444
--- /dev/null
+++ b/native/Avalonia.Native/src/OSX/menu.mm
@@ -0,0 +1,305 @@
+
+#include "common.h"
+#include "menu.h"
+
+@implementation AvnMenu
+@end
+
+@implementation AvnMenuItem
+{
+ AvnAppMenuItem* _item;
+}
+
+- (id) initWithAvnAppMenuItem: (AvnAppMenuItem*)menuItem
+{
+ if(self != nil)
+ {
+ _item = menuItem;
+ self = [super initWithTitle:@""
+ action:@selector(didSelectItem:)
+ keyEquivalent:@""];
+
+ [self setEnabled:YES];
+
+ [self setTarget:self];
+ }
+
+ return self;
+}
+
+- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
+{
+ if([self submenu] != nil)
+ {
+ return YES;
+ }
+
+ return _item->EvaluateItemEnabled();
+}
+
+- (void)didSelectItem:(nullable id)sender
+{
+ _item->RaiseOnClicked();
+}
+@end
+
+AvnAppMenuItem::AvnAppMenuItem(bool isSeperator)
+{
+ _isSeperator = isSeperator;
+
+ if(isSeperator)
+ {
+ _native = [NSMenuItem separatorItem];
+ }
+ else
+ {
+ _native = [[AvnMenuItem alloc] initWithAvnAppMenuItem: this];
+ }
+
+ _callback = nullptr;
+}
+
+NSMenuItem* AvnAppMenuItem::GetNative()
+{
+ return _native;
+}
+
+HRESULT AvnAppMenuItem::SetSubMenu (IAvnAppMenu* menu)
+{
+ auto nsMenu = dynamic_cast(menu)->GetNative();
+
+ [_native setSubmenu: nsMenu];
+
+ return S_OK;
+}
+
+HRESULT AvnAppMenuItem::SetTitle (void* utf8String)
+{
+ if (utf8String != nullptr)
+ {
+ [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]];
+ }
+
+ return S_OK;
+}
+
+HRESULT AvnAppMenuItem::SetGesture (void* key, AvnInputModifiers modifiers)
+{
+ NSEventModifierFlags flags = 0;
+
+ if (modifiers & Control)
+ flags |= NSEventModifierFlagControl;
+ if (modifiers & Shift)
+ flags |= NSEventModifierFlagShift;
+ if (modifiers & Alt)
+ flags |= NSEventModifierFlagOption;
+ if (modifiers & Windows)
+ flags |= NSEventModifierFlagCommand;
+
+ [_native setKeyEquivalent:[NSString stringWithUTF8String:(const char*)key]];
+ [_native setKeyEquivalentModifierMask:flags];
+
+ return S_OK;
+}
+
+HRESULT AvnAppMenuItem::SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback)
+{
+ _predicate = predicate;
+ _callback = callback;
+ return S_OK;
+}
+
+bool AvnAppMenuItem::EvaluateItemEnabled()
+{
+ if(_predicate != nullptr)
+ {
+ auto result = _predicate->Evaluate ();
+
+ return result;
+ }
+
+ return false;
+}
+
+void AvnAppMenuItem::RaiseOnClicked()
+{
+ if(_callback != nullptr)
+ {
+ _callback->Run();
+ }
+}
+
+AvnAppMenu::AvnAppMenu()
+{
+ _native = [AvnMenu new];
+}
+
+AvnAppMenu::AvnAppMenu(AvnMenu* native)
+{
+ _native = native;
+}
+
+AvnMenu* AvnAppMenu::GetNative()
+{
+ return _native;
+}
+
+HRESULT AvnAppMenu::AddItem (IAvnAppMenuItem* item)
+{
+ auto avnMenuItem = dynamic_cast(item);
+
+ if(avnMenuItem != nullptr)
+ {
+ [_native addItem: avnMenuItem->GetNative()];
+ }
+
+ return S_OK;
+}
+
+HRESULT AvnAppMenu::RemoveItem (IAvnAppMenuItem* item)
+{
+ auto avnMenuItem = dynamic_cast(item);
+
+ if(avnMenuItem != nullptr)
+ {
+ [_native removeItem:avnMenuItem->GetNative()];
+ }
+
+ return S_OK;
+}
+
+HRESULT AvnAppMenu::SetTitle (void* utf8String)
+{
+ if (utf8String != nullptr)
+ {
+ [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]];
+ }
+
+ return S_OK;
+}
+
+HRESULT AvnAppMenu::Clear()
+{
+ [_native removeAllItems];
+ return S_OK;
+}
+
+extern IAvnAppMenu* CreateAppMenu()
+{
+ @autoreleasepool
+ {
+ id menuBar = [NSMenu new];
+ return new AvnAppMenu(menuBar);
+ }
+}
+
+extern IAvnAppMenuItem* CreateAppMenuItem()
+{
+ @autoreleasepool
+ {
+ return new AvnAppMenuItem(false);
+ }
+}
+
+extern IAvnAppMenuItem* CreateAppMenuItemSeperator()
+{
+ @autoreleasepool
+ {
+ return new AvnAppMenuItem(true);
+ }
+}
+
+static IAvnAppMenu* s_appMenu = nullptr;
+static NSMenuItem* s_appMenuItem = nullptr;
+
+extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu)
+{
+ s_appMenu = menu;
+
+ if(s_appMenu != nullptr)
+ {
+ auto nativeMenu = dynamic_cast(s_appMenu);
+
+ auto currentMenu = [s_appMenuItem menu];
+
+ if (currentMenu != nullptr)
+ {
+ [currentMenu removeItem:s_appMenuItem];
+ }
+
+ s_appMenuItem = [nativeMenu->GetNative() itemAtIndex:0];
+
+ if (currentMenu == nullptr)
+ {
+ currentMenu = [s_appMenuItem menu];
+ }
+
+ [[s_appMenuItem menu] removeItem:s_appMenuItem];
+
+ [currentMenu insertItem:s_appMenuItem atIndex:0];
+
+ if([s_appMenuItem submenu] == nullptr)
+ {
+ [s_appMenuItem setSubmenu:[NSMenu new]];
+ }
+
+ auto appMenu = [s_appMenuItem submenu];
+
+ [appMenu addItem:[NSMenuItem separatorItem]];
+
+ // Services item and menu
+ auto servicesItem = [[NSMenuItem alloc] init];
+ servicesItem.title = @"Services";
+ NSMenu *servicesMenu = [[NSMenu alloc] initWithTitle:@"Services"];
+ servicesItem.submenu = servicesMenu;
+ [NSApplication sharedApplication].servicesMenu = servicesMenu;
+ [appMenu addItem:servicesItem];
+
+ [appMenu addItem:[NSMenuItem separatorItem]];
+
+ // Hide Application
+ auto hideItem = [[NSMenuItem alloc] initWithTitle:[@"Hide " stringByAppendingString:appName] action:@selector(hide:) keyEquivalent:@"h"];
+
+ [appMenu addItem:hideItem];
+
+ // Hide Others
+ auto hideAllOthersItem = [[NSMenuItem alloc] initWithTitle:@"Hide Others"
+ action:@selector(hideOtherApplications:)
+ keyEquivalent:@"h"];
+
+ hideAllOthersItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagOption;
+ [appMenu addItem:hideAllOthersItem];
+
+ // Show All
+ auto showAllItem = [[NSMenuItem alloc] initWithTitle:@"Show All"
+ action:@selector(unhideAllApplications:)
+ keyEquivalent:@""];
+
+ [appMenu addItem:showAllItem];
+
+ [appMenu addItem:[NSMenuItem separatorItem]];
+
+ // Quit Application
+ auto quitItem = [[NSMenuItem alloc] init];
+ quitItem.title = [@"Quit " stringByAppendingString:appName];
+ quitItem.keyEquivalent = @"q";
+ quitItem.action = @selector(terminate:);
+ [appMenu addItem:quitItem];
+ }
+ else
+ {
+ s_appMenuItem = nullptr;
+ }
+}
+
+extern IAvnAppMenu* GetAppMenu ()
+{
+ return s_appMenu;
+}
+
+extern NSMenuItem* GetAppMenuItem ()
+{
+ return s_appMenuItem;
+}
+
+
diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm
index 297097584a..e7abedae51 100644
--- a/native/Avalonia.Native/src/OSX/platformthreading.mm
+++ b/native/Avalonia.Native/src/OSX/platformthreading.mm
@@ -10,12 +10,6 @@ class PlatformThreadingInterface;
-(Signaler*) init;
@end
-
-@interface ActionCallback : NSObject
-- (ActionCallback*) initWithCallback: (IAvnActionCallback*) callback;
-- (void) action;
-@end
-
@implementation ActionCallback
{
ComPtr _callback;
diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h
index e2221217f3..557e19e7a8 100644
--- a/native/Avalonia.Native/src/OSX/window.h
+++ b/native/Avalonia.Native/src/OSX/window.h
@@ -20,6 +20,7 @@ class WindowBaseImpl;
-(void) pollModalSession: (NSModalSession _Nonnull) session;
-(void) restoreParentWindow;
-(bool) shouldTryToHandleEvents;
+-(void) applyMenu:(NSMenu *)menu;
@end
struct INSWindowHolder
diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm
index 3347d58004..dbb437243a 100644
--- a/native/Avalonia.Native/src/OSX/window.mm
+++ b/native/Avalonia.Native/src/OSX/window.mm
@@ -5,6 +5,7 @@
#include "window.h"
#include "KeyTransform.h"
#include "cursor.h"
+#include "menu.h"
#include
class SoftwareDrawingOperation
@@ -63,9 +64,11 @@ public:
SoftwareDrawingOperation CurrentSwDrawingOperation;
AvnPoint lastPositionSet;
NSString* _lastTitle;
+ IAvnAppMenu* _mainMenu;
WindowBaseImpl(IAvnWindowBaseEvents* events)
{
+ _mainMenu = nullptr;
BaseEvents = events;
View = [[AvnView alloc] initWithParent:this];
@@ -93,6 +96,7 @@ public:
UpdateStyle();
[Window makeKeyAndOrderFront:Window];
+ [NSApp activateIgnoringOtherApps:YES];
[Window setTitle:_lastTitle];
[Window setTitleVisibility:NSWindowTitleVisible];
@@ -122,6 +126,7 @@ public:
if(Window != nullptr)
{
[Window makeKeyWindow];
+ [NSApp activateIgnoringOtherApps:YES];
}
}
@@ -209,6 +214,31 @@ public:
}
}
+ virtual HRESULT SetMainMenu(IAvnAppMenu* menu) override
+ {
+ _mainMenu = menu;
+
+ auto nativeMenu = dynamic_cast(menu);
+
+ auto nsmenu = nativeMenu->GetNative();
+
+ [Window applyMenu:nsmenu];
+
+ return S_OK;
+ }
+
+ virtual HRESULT ObtainMainMenu(IAvnAppMenu** ret) override
+ {
+ if(ret == nullptr)
+ {
+ return E_POINTER;
+ }
+
+ *ret = _mainMenu;
+
+ return S_OK;
+ }
+
virtual bool TryLock() override
{
@autoreleasepool
@@ -1042,6 +1072,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
ComPtr _parent;
bool _canBecomeKeyAndMain;
bool _closed;
+ NSMenu* _menu;
+ bool _isAppMenuApplied;
}
- (void)dealloc
@@ -1065,6 +1097,32 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
}
}
+-(void) applyMenu:(NSMenu *)menu
+{
+ if(menu == nullptr)
+ {
+ menu = [NSMenu new];
+ }
+
+ _menu = menu;
+
+ if ([self isKeyWindow])
+ {
+ auto appMenu = ::GetAppMenuItem();
+
+ if(appMenu != nullptr)
+ {
+ [[appMenu menu] removeItem:appMenu];
+
+ [_menu insertItem:appMenu atIndex:0];
+
+ _isAppMenuApplied = true;
+ }
+
+ [NSApp setMenu:menu];
+ }
+}
+
-(void) setCanBecomeKeyAndMain
{
_canBecomeKeyAndMain = true;
@@ -1157,6 +1215,24 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
{
if([self activateAppropriateChild: true])
{
+ if(_menu == nullptr)
+ {
+ _menu = [NSMenu new];
+ }
+
+ auto appMenu = ::GetAppMenuItem();
+
+ if(appMenu != nullptr)
+ {
+ [[appMenu menu] removeItem:appMenu];
+
+ [_menu insertItem:appMenu atIndex:0];
+
+ _isAppMenuApplied = true;
+ }
+
+ [NSApp setMenu:_menu];
+
_parent->BaseEvents->Activated();
[super becomeKeyWindow];
}
@@ -1201,6 +1277,28 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
{
if(_parent)
_parent->BaseEvents->Deactivated();
+
+ auto appMenuItem = ::GetAppMenuItem();
+
+ if(appMenuItem != nullptr)
+ {
+ auto appMenu = ::GetAppMenu();
+
+ auto nativeAppMenu = dynamic_cast(appMenu);
+
+ [[appMenuItem menu] removeItem:appMenuItem];
+
+ [nativeAppMenu->GetNative() addItem:appMenuItem];
+
+ [NSApp setMenu:nativeAppMenu->GetNative()];
+ }
+ else
+ {
+ [NSApp setMenu:nullptr];
+ }
+
+ // remove window menu items from appmenu?
+
[super resignKeyWindow];
}
diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
index bc76a39f08..5b82e2caee 100644
--- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
+++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
@@ -16,7 +16,7 @@
Resources\Resource.Designer.cs
Off
False
- v8.0
+ v9.0
Properties\AndroidManifest.xml
diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs
index 157609088f..40d001a195 100644
--- a/samples/ControlCatalog.Android/MainActivity.cs
+++ b/samples/ControlCatalog.Android/MainActivity.cs
@@ -20,7 +20,7 @@ namespace ControlCatalog.Android
{
if (Avalonia.Application.Current == null)
{
- AppBuilder.Configure(new App())
+ AppBuilder.Configure()
.UseAndroid()
.SetupWithoutStarting();
Content = new MainView();
diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index d683092edf..854cae484c 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -55,7 +55,11 @@ namespace ControlCatalog.NetCore
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure()
.UsePlatformDetect()
- .With(new X11PlatformOptions { EnableMultiTouch = true })
+ .With(new X11PlatformOptions
+ {
+ EnableMultiTouch = true,
+ UseDBusMenu = true
+ })
.With(new Win32PlatformOptions
{
EnableMultitouch = true,
diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml
index 2f6d25c089..335c460b40 100644
--- a/samples/ControlCatalog/App.xaml
+++ b/samples/ControlCatalog/App.xaml
@@ -2,9 +2,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.App">
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs
index 07c42c60c4..4fc63ea054 100644
--- a/samples/ControlCatalog/App.xaml.cs
+++ b/samples/ControlCatalog/App.xaml.cs
@@ -1,4 +1,6 @@
+using System;
using Avalonia;
+using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
@@ -6,9 +8,20 @@ namespace ControlCatalog
{
public class App : Application
{
+ private NativeMenu _recentMenu;
+
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
+
+ Name = "Avalonia";
+
+ _recentMenu = (NativeMenu.GetMenu(this).Items[1] as NativeMenuItem).Menu;
+ }
+
+ public void OnOpenClicked(object sender, EventArgs args)
+ {
+ _recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1)));
}
public override void OnFrameworkInitializationCompleted()
diff --git a/samples/ControlCatalog/DecoratedWindow.xaml b/samples/ControlCatalog/DecoratedWindow.xaml
index cb6016b324..8e4c97b7f0 100644
--- a/samples/ControlCatalog/DecoratedWindow.xaml
+++ b/samples/ControlCatalog/DecoratedWindow.xaml
@@ -3,6 +3,31 @@
x:Class="ControlCatalog.DecoratedWindow"
Title="Avalonia Control Gallery"
xmlns:local="clr-namespace:ControlCatalog" HasSystemDecorations="False" Name="Window">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml
index 9527ac3b4e..6088f2ec57 100644
--- a/samples/ControlCatalog/MainWindow.xaml
+++ b/samples/ControlCatalog/MainWindow.xaml
@@ -8,7 +8,36 @@
xmlns:vm="clr-namespace:ControlCatalog.ViewModels"
xmlns:v="clr-namespace:ControlCatalog.Views"
x:Class="ControlCatalog.MainWindow">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs
index 95c65ed92f..7b0ee897c4 100644
--- a/samples/ControlCatalog/MainWindow.xaml.cs
+++ b/samples/ControlCatalog/MainWindow.xaml.cs
@@ -14,6 +14,7 @@ namespace ControlCatalog
public class MainWindow : Window
{
private WindowNotificationManager _notificationArea;
+ private NativeMenu _recentMenu;
public MainWindow()
{
@@ -29,8 +30,21 @@ namespace ControlCatalog
};
DataContext = new MainWindowViewModel(_notificationArea);
+ _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Menu;
}
+ public void OnOpenClicked(object sender, EventArgs args)
+ {
+ _recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1)));
+ }
+
+ public void OnCloseClicked(object sender, EventArgs args)
+ {
+ Close();
+ }
+
+
+
private void InitializeComponent()
{
// TODO: iOS does not support dynamically loading assemblies
diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml
index e1a5cf2c5a..868f0df6ad 100644
--- a/samples/ControlCatalog/Pages/MenuPage.xaml
+++ b/samples/ControlCatalog/Pages/MenuPage.xaml
@@ -3,8 +3,11 @@
x:Class="ControlCatalog.Pages.MenuPage">
Menu
+ Exported menu fallback
+ (Should be only visible on platforms without desktop-global menu bar)
+
A window menu
-
+
(this T builder) where T : AppBuilderBase, new()
{
- builder.UseWindowingSubsystem(() => Android.AndroidPlatform.Initialize(builder.Instance), "Android");
+ builder.UseWindowingSubsystem(() => Android.AndroidPlatform.Initialize(builder.ApplicationType), "Android");
builder.UseSkia();
return builder;
}
@@ -41,7 +41,7 @@ namespace Avalonia.Android
_scalingFactor = global::Android.App.Application.Context.Resources.DisplayMetrics.ScaledDensity;
}
- public static void Initialize(Avalonia.Application app)
+ public static void Initialize(Type appType)
{
AvaloniaLocator.CurrentMutable
.Bind().ToTransient()
@@ -55,7 +55,7 @@ namespace Avalonia.Android
.Bind().ToConstant(new DefaultRenderTimer(60))
.Bind().ToConstant(new RenderLoop())
.Bind().ToSingleton()
- .Bind().ToConstant(new AssetLoader(app.GetType().Assembly));
+ .Bind().ToConstant(new AssetLoader(appType.Assembly));
SkiaPlatform.Initialize();
((global::Android.App.Application) global::Android.App.Application.Context.ApplicationContext)
diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj
index 0089ea3b8d..c170e8449c 100644
--- a/src/Android/Avalonia.Android/Avalonia.Android.csproj
+++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj
@@ -1,6 +1,6 @@
- monoandroid80
+ monoandroid90
true
diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
index 1b2b205d45..2f95a6e4bd 100644
--- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
+++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
@@ -16,7 +16,7 @@
Resources\Resource.Designer.cs
Off
False
- v8.0
+ v9.0
Properties\AndroidManifest.xml
diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs
index 307ddd284c..d9be9171ed 100644
--- a/src/Avalonia.Controls/AppBuilderBase.cs
+++ b/src/Avalonia.Controls/AppBuilderBase.cs
@@ -18,7 +18,9 @@ namespace Avalonia.Controls
{
private static bool s_setupWasAlreadyCalled;
private Action _optionsInitializers;
-
+ private Func _appFactory;
+ private IApplicationLifetime _lifetime;
+
///
/// Gets or sets the instance.
///
@@ -30,10 +32,15 @@ namespace Avalonia.Controls
public Action RuntimePlatformServicesInitializer { get; private set; }
///
- /// Gets or sets the instance being initialized.
+ /// Gets the instance being initialized.
///
- public Application Instance { get; protected set; }
-
+ public Application Instance { get; private set; }
+
+ ///
+ /// Gets the type of the Instance (even if it's not created yet)
+ ///
+ public Type ApplicationType { get; private set; }
+
///
/// Gets or sets a method to call the initialize the windowing subsystem.
///
@@ -76,20 +83,11 @@ namespace Avalonia.Controls
public static TAppBuilder Configure()
where TApp : Application, new()
{
- return Configure(new TApp());
- }
-
- ///
- /// Begin configuring an .
- ///
- /// An instance.
- public static TAppBuilder Configure(Application app)
- {
- AvaloniaLocator.CurrentMutable.BindToSelf(app);
-
return new TAppBuilder()
{
- Instance = app,
+ ApplicationType = typeof(TApp),
+ // Needed for CoreRT compatibility
+ _appFactory = () => new TApp()
};
}
@@ -157,6 +155,18 @@ namespace Avalonia.Controls
return Self;
}
+ ///
+ /// Sets up the platform-specific services for the application and initialized it with a particular lifetime, but does not run it.
+ ///
+ ///
+ ///
+ public TAppBuilder SetupWithLifetime(IApplicationLifetime lifetime)
+ {
+ _lifetime = lifetime;
+ Setup();
+ return Self;
+ }
+
///
/// Specifies a windowing subsystem to use.
///
@@ -254,11 +264,6 @@ namespace Avalonia.Controls
///
private void Setup()
{
- if (Instance == null)
- {
- throw new InvalidOperationException("No App instance configured.");
- }
-
if (RuntimePlatformServicesInitializer == null)
{
throw new InvalidOperationException("No runtime platform services configured.");
@@ -285,6 +290,9 @@ namespace Avalonia.Controls
WindowingSubsystemInitializer();
RenderingSubsystemInitializer();
AfterPlatformServicesSetupCallback(Self);
+ Instance = _appFactory();
+ Instance.ApplicationLifetime = _lifetime;
+ AvaloniaLocator.CurrentMutable.BindToSelf(Instance);
Instance.RegisterServices();
Instance.Initialize();
AfterSetupCallback(Self);
diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs
index 382106de65..ce60a0f0b9 100644
--- a/src/Avalonia.Controls/Application.cs
+++ b/src/Avalonia.Controls/Application.cs
@@ -32,7 +32,7 @@ namespace Avalonia
/// method.
/// - Tracks the lifetime of the application.
///
- public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode
+ public class Application : AvaloniaObject, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode
{
///
/// The application-global data templates.
@@ -210,5 +210,22 @@ namespace Avalonia
{
ResourcesChanged?.Invoke(this, e);
}
+
+ private string _name;
+ ///
+ /// Defines Name property
+ ///
+ public static readonly DirectProperty NameProperty =
+ AvaloniaProperty.RegisterDirect("Name", o => o.Name, (o, v) => o.Name = v);
+
+ ///
+ /// Application name to be used for various platform-specific purposes
+ ///
+ public string Name
+ {
+ get => _name;
+ set => SetAndRaise(NameProperty, ref _name, value);
+ }
+
}
}
diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
index abca7a64ee..2533191ae4 100644
--- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
+++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
@@ -125,8 +125,7 @@ namespace Avalonia
where T : AppBuilderBase, new()
{
var lifetime = new ClassicDesktopStyleApplicationLifetime(builder.Instance) {ShutdownMode = shutdownMode};
- builder.Instance.ApplicationLifetime = lifetime;
- builder.SetupWithoutStarting();
+ builder.SetupWithLifetime(lifetime);
return lifetime.Start(args);
}
}
diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs
new file mode 100644
index 0000000000..5d3a4526cc
--- /dev/null
+++ b/src/Avalonia.Controls/NativeMenu.Export.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Controls.Platform;
+using Avalonia.Data;
+
+namespace Avalonia.Controls
+{
+ public partial class NativeMenu
+ {
+ public static readonly AttachedProperty IsNativeMenuExportedProperty =
+ AvaloniaProperty.RegisterAttached("IsNativeMenuExported");
+
+ public static bool GetIsNativeMenuExported(TopLevel tl) => tl.GetValue(IsNativeMenuExportedProperty);
+
+ private static readonly AttachedProperty s_nativeMenuInfoProperty =
+ AvaloniaProperty.RegisterAttached("___NativeMenuInfo");
+
+ class NativeMenuInfo
+ {
+ public bool ChangingIsExported { get; set; }
+ public ITopLevelNativeMenuExporter Exporter { get; }
+
+ public NativeMenuInfo(TopLevel target)
+ {
+ Exporter = (target.PlatformImpl as ITopLevelImplWithNativeMenuExporter)?.NativeMenuExporter;
+ if (Exporter != null)
+ {
+ Exporter.OnIsNativeMenuExportedChanged += delegate
+ {
+ SetIsNativeMenuExported(target, Exporter.IsNativeMenuExported);
+ };
+ }
+ }
+ }
+
+ static NativeMenuInfo GetInfo(TopLevel target)
+ {
+ var rv = target.GetValue(s_nativeMenuInfoProperty);
+ if (rv == null)
+ {
+ target.SetValue(s_nativeMenuInfoProperty, rv = new NativeMenuInfo(target));
+ SetIsNativeMenuExported(target, rv.Exporter?.IsNativeMenuExported ?? false);
+ }
+
+ return rv;
+ }
+
+ static void SetIsNativeMenuExported(TopLevel tl, bool value)
+ {
+ GetInfo(tl).ChangingIsExported = true;
+ tl.SetValue(IsNativeMenuExportedProperty, value);
+ }
+
+ public static readonly AttachedProperty MenuProperty
+ = AvaloniaProperty.RegisterAttached("Menu", validate:
+ (o, v) =>
+ {
+ if(!(o is Application || o is TopLevel))
+ throw new InvalidOperationException("NativeMenu.Menu property isn't valid on "+o.GetType());
+ return v;
+ });
+
+ public static void SetMenu(AvaloniaObject o, NativeMenu menu) => o.SetValue(MenuProperty, menu);
+ public static NativeMenu GetMenu(AvaloniaObject o) => o.GetValue(MenuProperty);
+
+ static NativeMenu()
+ {
+ // This is needed because of the lack of attached direct properties
+ IsNativeMenuExportedProperty.Changed.Subscribe(args =>
+ {
+ var info = GetInfo((TopLevel)args.Sender);
+ if (!info.ChangingIsExported)
+ throw new InvalidOperationException("IsNativeMenuExported property is read-only");
+ info.ChangingIsExported = false;
+ });
+ MenuProperty.Changed.Subscribe(args =>
+ {
+ if (args.Sender is TopLevel tl)
+ {
+ GetInfo(tl).Exporter?.SetNativeMenu((NativeMenu)args.NewValue);
+ }
+ });
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs
new file mode 100644
index 0000000000..54aa2b5e3d
--- /dev/null
+++ b/src/Avalonia.Controls/NativeMenu.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using Avalonia.Collections;
+using Avalonia.Data;
+using Avalonia.LogicalTree;
+using Avalonia.Metadata;
+
+namespace Avalonia.Controls
+{
+ public partial class NativeMenu : AvaloniaObject, IEnumerable
+ {
+ private readonly AvaloniaList _items =
+ new AvaloniaList { ResetBehavior = ResetBehavior.Remove };
+ private NativeMenuItem _parent;
+ [Content]
+ public IList Items => _items;
+
+ public NativeMenu()
+ {
+ _items.Validate = Validator;
+ _items.CollectionChanged += ItemsChanged;
+ }
+
+ private void Validator(NativeMenuItemBase obj)
+ {
+ if (obj.Parent != null)
+ throw new InvalidOperationException("NativeMenuItem already has a parent");
+ }
+
+ private void ItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ if(e.OldItems!=null)
+ foreach (NativeMenuItemBase i in e.OldItems)
+ i.Parent = null;
+ if(e.NewItems!=null)
+ foreach (NativeMenuItemBase i in e.NewItems)
+ i.Parent = this;
+ }
+
+ public static readonly DirectProperty ParentProperty =
+ AvaloniaProperty.RegisterDirect("Parent", o => o.Parent, (o, v) => o.Parent = v);
+
+ public NativeMenuItem Parent
+ {
+ get => _parent;
+ set => SetAndRaise(ParentProperty, ref _parent, value);
+ }
+
+ public void Add(NativeMenuItemBase item) => _items.Add(item);
+
+ public IEnumerator GetEnumerator() => _items.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/NativeMenuBar.cs b/src/Avalonia.Controls/NativeMenuBar.cs
new file mode 100644
index 0000000000..9b96ab9c8c
--- /dev/null
+++ b/src/Avalonia.Controls/NativeMenuBar.cs
@@ -0,0 +1,36 @@
+using System;
+using Avalonia.Controls.Primitives;
+using Avalonia.Interactivity;
+using Avalonia.Styling;
+
+namespace Avalonia.Controls
+{
+ public class NativeMenuBar : TemplatedControl
+ {
+ public static readonly AttachedProperty EnableMenuItemClickForwardingProperty =
+ AvaloniaProperty.RegisterAttached(
+ "EnableMenuItemClickForwarding");
+
+ static NativeMenuBar()
+ {
+ EnableMenuItemClickForwardingProperty.Changed.Subscribe(args =>
+ {
+ var item = (MenuItem)args.Sender;
+ if (args.NewValue.Equals(true))
+ item.Click += OnMenuItemClick;
+ else
+ item.Click -= OnMenuItemClick;
+ });
+ }
+
+ public static void SetEnableMenuItemClickForwarding(MenuItem menuItem, bool enable)
+ {
+ menuItem.SetValue(EnableMenuItemClickForwardingProperty, enable);
+ }
+
+ private static void OnMenuItemClick(object sender, RoutedEventArgs e)
+ {
+ (((MenuItem)sender).DataContext as NativeMenuItem)?.RaiseClick();
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs
new file mode 100644
index 0000000000..c1144d45b2
--- /dev/null
+++ b/src/Avalonia.Controls/NativeMenuItem.cs
@@ -0,0 +1,162 @@
+using System;
+using System.Windows.Input;
+using Avalonia.Input;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls
+{
+ public class NativeMenuItem : NativeMenuItemBase
+ {
+ private string _header;
+ private KeyGesture _gesture;
+ private bool _enabled = true;
+
+ private NativeMenu _menu;
+
+ static NativeMenuItem()
+ {
+ MenuProperty.Changed.Subscribe(args =>
+ {
+ var item = (NativeMenuItem)args.Sender;
+ var value = (NativeMenu)args.NewValue;
+ if (value.Parent != null && value.Parent != item)
+ throw new InvalidOperationException("NativeMenu already has a parent");
+ value.Parent = item;
+ });
+ }
+
+
+ class CanExecuteChangedSubscriber : IWeakSubscriber
+ {
+ private readonly NativeMenuItem _parent;
+
+ public CanExecuteChangedSubscriber(NativeMenuItem parent)
+ {
+ _parent = parent;
+ }
+
+ public void OnEvent(object sender, EventArgs e)
+ {
+ _parent.CanExecuteChanged();
+ }
+ }
+
+ private readonly CanExecuteChangedSubscriber _canExecuteChangedSubscriber;
+
+
+ public NativeMenuItem()
+ {
+ _canExecuteChangedSubscriber = new CanExecuteChangedSubscriber(this);
+ }
+
+ public NativeMenuItem(string header) : this()
+ {
+ Header = header;
+ }
+
+ public static readonly DirectProperty MenuProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Menu), o => o._menu,
+ (o, v) =>
+ {
+ if (v.Parent != null && v.Parent != o)
+ throw new InvalidOperationException("NativeMenu already has a parent");
+ o._menu = v;
+ });
+
+ public NativeMenu Menu
+ {
+ get => _menu;
+ set
+ {
+ if (value.Parent != null && value.Parent != this)
+ throw new InvalidOperationException("NativeMenu already has a parent");
+ SetAndRaise(MenuProperty, ref _menu, value);
+ }
+ }
+
+ public static readonly DirectProperty HeaderProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Header), o => o._header, (o, v) => o._header = v);
+
+ public string Header
+ {
+ get => GetValue(HeaderProperty);
+ set => SetValue(HeaderProperty, value);
+ }
+
+ public static readonly DirectProperty GestureProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Gesture), o => o._gesture, (o, v) => o._gesture = v);
+
+ public KeyGesture Gesture
+ {
+ get => GetValue(GestureProperty);
+ set => SetValue(GestureProperty, value);
+ }
+
+ private ICommand _command;
+
+ public static readonly DirectProperty CommandProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Command),
+ o => o._command, (o, v) =>
+ {
+ if (o._command != null)
+ WeakSubscriptionManager.Unsubscribe(o._command,
+ nameof(ICommand.CanExecuteChanged), o._canExecuteChangedSubscriber);
+ o._command = v;
+ if (o._command != null)
+ WeakSubscriptionManager.Subscribe(o._command,
+ nameof(ICommand.CanExecuteChanged), o._canExecuteChangedSubscriber);
+ o.CanExecuteChanged();
+ });
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty
public AppBuilder()
: base(new StandardRuntimePlatform(),
- builder => StandardRuntimePlatformServices.Register(builder.Instance?.GetType()?.Assembly))
+ builder => StandardRuntimePlatformServices.Register(builder.ApplicationType.Assembly))
{
}
- ///
- /// Initializes a new instance of the class.
- ///
- /// The instance.
- public AppBuilder(Application app) : this()
- {
- Instance = app;
- }
-
bool CheckEnvironment(Type checkerType)
{
if (checkerType == null)
diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
index d7e1d8cdb3..d9fd3b78a2 100644
--- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
+++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
@@ -8,4 +8,8 @@
+
+
+
+
diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs
new file mode 100644
index 0000000000..b445f86613
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusHelper.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Threading;
+using Avalonia.Threading;
+using Tmds.DBus;
+
+namespace Avalonia.FreeDesktop
+{
+ public class DBusHelper
+ {
+ ///
+ /// This class uses synchronous execution at DBus connection establishment stage
+ /// then switches to using AvaloniaSynchronizationContext
+ ///
+ class DBusSyncContext : SynchronizationContext
+ {
+ private SynchronizationContext _ctx;
+ private object _lock = new object();
+
+ public override void Post(SendOrPostCallback d, object state)
+ {
+ lock (_lock)
+ {
+ if (_ctx != null)
+ _ctx?.Post(d, state);
+ else
+ lock (_lock)
+ d(state);
+ }
+ }
+
+ public override void Send(SendOrPostCallback d, object state)
+ {
+ lock (_lock)
+ {
+ if (_ctx != null)
+ _ctx?.Send(d, state);
+ else
+
+ d(state);
+ }
+ }
+
+ public void Initialized()
+ {
+ lock (_lock)
+ _ctx = new AvaloniaSynchronizationContext();
+ }
+ }
+ public static Connection Connection { get; private set; }
+
+ public static Exception TryInitialize(string dbusAddress = null)
+ {
+ var oldContext = SynchronizationContext.Current;
+ try
+ {
+
+ var dbusContext = new DBusSyncContext();
+ SynchronizationContext.SetSynchronizationContext(dbusContext);
+ var conn = new Connection(new ClientConnectionOptions(dbusAddress ?? Address.Session)
+ {
+ AutoConnect = false,
+ SynchronizationContext = dbusContext
+ });
+ // Connect synchronously
+ conn.ConnectAsync().Wait();
+
+ // Initialize a brand new sync-context
+ dbusContext.Initialized();
+ Connection = conn;
+ }
+ catch (Exception e)
+ {
+ return e;
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(oldContext);
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusMenu.cs b/src/Avalonia.FreeDesktop/DBusMenu.cs
new file mode 100644
index 0000000000..7180345386
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusMenu.cs
@@ -0,0 +1,56 @@
+
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using Tmds.DBus;
+
+[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)]
+namespace Avalonia.FreeDesktop.DBusMenu
+{
+
+ [DBusInterface("org.freedesktop.DBus.Properties")]
+ interface IFreeDesktopDBusProperties : IDBusObject
+ {
+ Task