diff --git a/native/Avalonia.Native/inc/IGetNative.h b/native/Avalonia.Native/inc/IGetNative.h new file mode 100644 index 0000000000..85ae030d74 --- /dev/null +++ b/native/Avalonia.Native/inc/IGetNative.h @@ -0,0 +1,10 @@ +#ifndef igetnative_h +#define igetnative_h + +class IGetNative +{ +public: + virtual void* GetNative() = 0; +}; + +#endif diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index e54f3fa6a7..16d4fa4107 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,9 @@ public: virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0; virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0; virtual HRESULT ObtainGlFeature(IAvnGlFeature** ppv) = 0; + virtual HRESULT ObtainAppMenu (IAvnAppMenu** ppv) = 0; + virtual HRESULT CreateMenu (IAvnAppMenu** ppv) = 0; + virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) = 0; }; AVNCOM(IAvnString, 17) : IUnknown @@ -258,6 +263,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 +373,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..84c3a84b91 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 @@ -12,6 +12,7 @@ 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 */; }; @@ -32,6 +33,8 @@ 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 = ""; }; + 5296D43022F30EBC005B125D /* menu.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = menu.h; path = ../../../../../../../../System/Volumes/Data/Users/danwalmsley/repos/Avalonia/native/Avalonia.Native/src/OSX/menu.h; 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 +88,8 @@ AB661C1F2148286E00291242 /* window.mm */, 37C09D8A21581EF2006A6758 /* window.h */, AB00E4F62147CA920032A60A /* main.mm */, + 520624B222973F4100C4DCEF /* menu.mm */, + 5296D43022F30EBC005B125D /* menu.h */, 37A517B22159597E00FBA241 /* Screens.mm */, 37C09D8721580FE4006A6758 /* SystemDialogs.mm */, AB7A61F02147C815003C5833 /* Products */, @@ -150,6 +155,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = AB7A61E62147C814003C5833; @@ -173,6 +179,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..c066ebb498 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -19,6 +19,10 @@ extern IAvnClipboard* CreateClipboard(); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlFeature* GetGlFeature(); extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* view); +extern IAvnAppMenu* GetAppMenu(); +extern IAvnAppMenu* CreateAppMenu(); +extern IAvnAppMenuItem* CreateAppMenuItem(); + extern void InitializeAvnApp(); extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; extern NSPoint ToNSPoint (AvnPoint p); @@ -40,4 +44,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..159f01d1d7 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -5,10 +5,115 @@ #define COM_GUIDS_MATERIALIZE #include "common.h" +// 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(CFStringRef process_name) { + 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]; + + CFStringRef titleRef = (__bridge CFStringRef)appTitle; + SetProcessName(titleRef); + + return S_OK; + } + virtual HRESULT SetShowInDock(int show) override { AvnDesiredActivationPolicy = show @@ -17,8 +122,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 +146,6 @@ public: close(_fds[0]); close(_fds[1]); } - - @end @@ -123,6 +224,24 @@ public: *ppv = rv; return S_OK; } + + virtual HRESULT ObtainAppMenu(IAvnAppMenu** ppv) override + { + *ppv = ::GetAppMenu(); + 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; + } }; 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..e3c1fa9768 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -0,0 +1,79 @@ +// +// 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 + +@end + +@interface AvnMenuItem : NSMenuItem +- (id) initWithAvnAppMenuItem: (AvnAppMenuItem*)menuItem; +- (void)didSelectItem:(id)sender; +@end + +class AvnAppMenuItem : public ComSingleObject +{ +private: + AvnMenuItem* _native; // here we hold a pointer to an AvnMenuItem + IAvnActionCallback* _callback; + IAvnPredicateCallback* _predicate; + +public: + FORWARD_IUNKNOWN() + + AvnAppMenuItem(); + + AvnMenuItem* 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..f861774b29 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -0,0 +1,211 @@ + +#include "common.h" +#include "IGetNative.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() +{ + _native = [[AvnMenuItem alloc] initWithAvnAppMenuItem: this]; + _callback = nullptr; +} + +AvnMenuItem* 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) +{ + [_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) +{ + [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + + return S_OK; +} + +HRESULT AvnAppMenu::Clear() +{ + [_native removeAllItems]; + return S_OK; +} + +static IAvnAppMenu* s_AppMenu = nullptr; + +extern IAvnAppMenu* GetAppMenu() +{ + @autoreleasepool + { + if(s_AppMenu == nullptr) + { + id menubar = [NSMenu new]; + [menubar setTitle:@"Test"]; + [NSApp setMainMenu:menubar]; + + id appMenuItem = [AvnMenuItem new]; + [[NSApp mainMenu] addItem:appMenuItem]; + + [appMenuItem setSubmenu:[AvnMenu new]]; + + s_AppMenu = new AvnAppMenu([[NSApplication sharedApplication] mainMenu]); + } + + return s_AppMenu; + } +} + +extern IAvnAppMenu* CreateAppMenu() +{ + @autoreleasepool + { + return new AvnAppMenu(); + } +} + +extern IAvnAppMenuItem* CreateAppMenuItem() +{ + @autoreleasepool + { + return new AvnAppMenuItem(); + } +} 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;