diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 3475eff654..757c0bc85a 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -19,8 +19,9 @@ struct IAvnGlContext; struct IAvnGlDisplay; struct IAvnGlSurfaceRenderTarget; struct IAvnGlSurfaceRenderingSession; -struct IAvnAppMenu; -struct IAvnAppMenuItem; +struct IAvnMenu; +struct IAvnMenuItem; +struct IAvnMenuEvents; enum SystemDecorations { SystemDecorationsNone = 0, @@ -188,11 +189,10 @@ public: virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0; virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0; virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** 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; + virtual HRESULT SetAppMenu(IAvnMenu* menu) = 0; + virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) = 0; + virtual HRESULT CreateMenuItem (IAvnMenuItem** ppv) = 0; + virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) = 0; }; AVNCOM(IAvnString, 17) : IUnknown @@ -222,8 +222,7 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown virtual HRESULT SetTopMost (bool value) = 0; virtual HRESULT SetCursor(IAvnCursor* cursor) = 0; virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ret) = 0; - virtual HRESULT SetMainMenu(IAvnAppMenu* menu) = 0; - virtual HRESULT ObtainMainMenu(IAvnAppMenu** retOut) = 0; + virtual HRESULT SetMainMenu(IAvnMenu* menu) = 0; virtual HRESULT ObtainNSWindowHandle(void** retOut) = 0; virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0; virtual HRESULT ObtainNSViewHandle(void** retOut) = 0; @@ -388,10 +387,10 @@ AVNCOM(IAvnGlSurfaceRenderingSession, 16) : IUnknown virtual HRESULT GetScaling(double* ret) = 0; }; -AVNCOM(IAvnAppMenu, 17) : IUnknown +AVNCOM(IAvnMenu, 17) : IUnknown { - virtual HRESULT AddItem (IAvnAppMenuItem* item) = 0; - virtual HRESULT RemoveItem (IAvnAppMenuItem* item) = 0; + virtual HRESULT InsertItem (int index, IAvnMenuItem* item) = 0; + virtual HRESULT RemoveItem (IAvnMenuItem* item) = 0; virtual HRESULT SetTitle (void* utf8String) = 0; virtual HRESULT Clear () = 0; }; @@ -401,12 +400,21 @@ AVNCOM(IAvnPredicateCallback, 18) : IUnknown virtual bool Evaluate() = 0; }; -AVNCOM(IAvnAppMenuItem, 19) : IUnknown +AVNCOM(IAvnMenuItem, 19) : IUnknown { - virtual HRESULT SetSubMenu (IAvnAppMenu* menu) = 0; + virtual HRESULT SetSubMenu (IAvnMenu* menu) = 0; virtual HRESULT SetTitle (void* utf8String) = 0; virtual HRESULT SetGesture (void* utf8String, AvnInputModifiers modifiers) = 0; virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) = 0; + virtual HRESULT SetIsChecked (bool isChecked) = 0; +}; + +AVNCOM(IAvnMenuEvents, 1A) : IUnknown +{ + /** + * NeedsUpdate + */ + virtual void NeedsUpdate () = 0; }; extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative(); diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme index 1a665d3ea5..5d20a135b9 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme @@ -29,8 +29,6 @@ shouldUseLaunchSchemeArgsEnv = "YES"> - - - - @end -extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular; +NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular; + @implementation AvnAppDelegate - (void)applicationWillFinishLaunching:(NSNotification *)notification { diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 85403abfe7..7a433bfd9f 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -15,11 +15,11 @@ extern IAvnScreens* CreateScreens(); extern IAvnClipboard* CreateClipboard(); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlDisplay* GetGlDisplay(); -extern IAvnAppMenu* CreateAppMenu(); -extern IAvnAppMenuItem* CreateAppMenuItem(); -extern IAvnAppMenuItem* CreateAppMenuItemSeperator(); -extern void SetAppMenu (NSString* appName, IAvnAppMenu* appMenu); -extern IAvnAppMenu* GetAppMenu (); +extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events); +extern IAvnMenuItem* CreateAppMenuItem(); +extern IAvnMenuItem* CreateAppMenuItemSeperator(); +extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu); +extern IAvnMenu* GetAppMenu (); extern NSMenuItem* GetAppMenuItem (); extern void InitializeAvnApp(); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index a2134de6c1..a63353bc0a 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -92,12 +92,11 @@ void SetProcessName(NSString* appTitle) { 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 @@ -228,41 +227,29 @@ public: return S_OK; } - virtual HRESULT CreateMenu (IAvnAppMenu** ppv) override + virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) override { - *ppv = ::CreateAppMenu(); + *ppv = ::CreateAppMenu(cb); return S_OK; } - virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) override + virtual HRESULT CreateMenuItem (IAvnMenuItem** ppv) override { *ppv = ::CreateAppMenuItem(); return S_OK; } - virtual HRESULT CreateMenuItemSeperator (IAvnAppMenuItem** ppv) override + virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) override { *ppv = ::CreateAppMenuItemSeperator(); return S_OK; } - virtual HRESULT SetAppMenu (IAvnAppMenu* appMenu) override + virtual HRESULT SetAppMenu (IAvnMenu* 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 index befbe6a7e0..228731cb28 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -14,8 +14,10 @@ class AvnAppMenuItem; class AvnAppMenu; -@interface AvnMenu : NSMenu // for some reason it doesnt detect nsmenu here but compiler doesnt complain -- (void)setMenu:(NSMenu*) menu; +@interface AvnMenu : NSMenu +- (id) initWithDelegate: (NSObject*) del; +- (void) setHasGlobalMenuItem: (bool) value; +- (bool) hasGlobalMenuItem; @end @interface AvnMenuItem : NSMenuItem @@ -23,7 +25,7 @@ class AvnAppMenu; - (void)didSelectItem:(id)sender; @end -class AvnAppMenuItem : public ComSingleObject +class AvnAppMenuItem : public ComSingleObject { private: NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem @@ -38,7 +40,7 @@ public: NSMenuItem* GetNative(); - virtual HRESULT SetSubMenu (IAvnAppMenu* menu) override; + virtual HRESULT SetSubMenu (IAvnMenu* menu) override; virtual HRESULT SetTitle (void* utf8String) override; @@ -46,29 +48,32 @@ public: virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) override; + virtual HRESULT SetIsChecked (bool isChecked) override; + bool EvaluateItemEnabled(); void RaiseOnClicked(); }; -class AvnAppMenu : public ComSingleObject +class AvnAppMenu : public ComSingleObject { private: AvnMenu* _native; + ComPtr _baseEvents; public: FORWARD_IUNKNOWN() - AvnAppMenu(); - - AvnAppMenu(AvnMenu* native); - + AvnAppMenu(IAvnMenuEvents* events); + AvnMenu* GetNative(); - virtual HRESULT AddItem (IAvnAppMenuItem* item) override; + void RaiseNeedsUpdate (); - virtual HRESULT RemoveItem (IAvnAppMenuItem* item) override; + virtual HRESULT InsertItem (int index, IAvnMenuItem* item) override; + + virtual HRESULT RemoveItem (IAvnMenuItem* item) override; virtual HRESULT SetTitle (void* utf8String) override; @@ -76,5 +81,9 @@ public: }; +@interface AvnMenuDelegate : NSObject +- (id) initWithParent: (AvnAppMenu*) parent; +@end + #endif diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 1d2f075ccb..1cd9cb0644 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -4,6 +4,30 @@ #include "window.h" @implementation AvnMenu +{ + bool _isReparented; + NSObject* _wtf; +} + +- (id) initWithDelegate: (NSObject*)del +{ + self = [super init]; + self.delegate = del; + _wtf = del; + _isReparented = false; + return self; +} + +- (bool)hasGlobalMenuItem +{ + return _isReparented; +} + +- (void)setHasGlobalMenuItem:(bool)value +{ + _isReparented = value; +} + @end @implementation AvnMenuItem @@ -65,49 +89,77 @@ NSMenuItem* AvnAppMenuItem::GetNative() return _native; } -HRESULT AvnAppMenuItem::SetSubMenu (IAvnAppMenu* menu) +HRESULT AvnAppMenuItem::SetSubMenu (IAvnMenu* menu) { - auto nsMenu = dynamic_cast(menu)->GetNative(); - - [_native setSubmenu: nsMenu]; - - return S_OK; + @autoreleasepool + { + if(menu != nullptr) + { + auto nsMenu = dynamic_cast(menu)->GetNative(); + + [_native setSubmenu: nsMenu]; + } + else + { + [_native setSubmenu: nullptr]; + } + + return S_OK; + } } HRESULT AvnAppMenuItem::SetTitle (void* utf8String) { - if (utf8String != nullptr) + @autoreleasepool { - [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + if (utf8String != nullptr) + { + [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + } + + return S_OK; } - - 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; + @autoreleasepool + { + 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; + @autoreleasepool + { + _predicate = predicate; + _callback = callback; + return S_OK; + } +} + +HRESULT AvnAppMenuItem::SetIsChecked (bool isChecked) +{ + @autoreleasepool + { + [_native setState:(isChecked ? NSOnState : NSOffState)]; + return S_OK; + } } bool AvnAppMenuItem::EvaluateItemEnabled() @@ -130,71 +182,123 @@ void AvnAppMenuItem::RaiseOnClicked() } } -AvnAppMenu::AvnAppMenu() +AvnAppMenu::AvnAppMenu(IAvnMenuEvents* events) { - _native = [AvnMenu new]; + _baseEvents = events; + id del = [[AvnMenuDelegate alloc] initWithParent: this]; + _native = [[AvnMenu alloc] initWithDelegate: del]; } -AvnAppMenu::AvnAppMenu(AvnMenu* native) -{ - _native = native; -} AvnMenu* AvnAppMenu::GetNative() { return _native; } -HRESULT AvnAppMenu::AddItem (IAvnAppMenuItem* item) +void AvnAppMenu::RaiseNeedsUpdate() { - auto avnMenuItem = dynamic_cast(item); - - if(avnMenuItem != nullptr) + if(_baseEvents != nullptr) { - [_native addItem: avnMenuItem->GetNative()]; + _baseEvents->NeedsUpdate(); } - - return S_OK; } -HRESULT AvnAppMenu::RemoveItem (IAvnAppMenuItem* item) +HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item) { - auto avnMenuItem = dynamic_cast(item); - - if(avnMenuItem != nullptr) + @autoreleasepool { - [_native removeItem:avnMenuItem->GetNative()]; + if([_native hasGlobalMenuItem]) + { + index++; + } + + auto avnMenuItem = dynamic_cast(item); + + if(avnMenuItem != nullptr) + { + [_native insertItem: avnMenuItem->GetNative() atIndex:index]; + } + + return S_OK; + } +} + +HRESULT AvnAppMenu::RemoveItem (IAvnMenuItem* item) +{ + @autoreleasepool + { + auto avnMenuItem = dynamic_cast(item); + + if(avnMenuItem != nullptr) + { + [_native removeItem:avnMenuItem->GetNative()]; + } + + return S_OK; } - - return S_OK; } HRESULT AvnAppMenu::SetTitle (void* utf8String) { - if (utf8String != nullptr) + @autoreleasepool { - [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + if (utf8String != nullptr) + { + [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + } + + return S_OK; } - - return S_OK; } HRESULT AvnAppMenu::Clear() { - [_native removeAllItems]; - return S_OK; + @autoreleasepool + { + [_native removeAllItems]; + return S_OK; + } +} + +@implementation AvnMenuDelegate +{ + ComPtr _parent; +} +- (id) initWithParent:(AvnAppMenu *)parent +{ + self = [super init]; + _parent = parent; + return self; } +- (BOOL)menu:(NSMenu *)menu updateItem:(NSMenuItem *)item atIndex:(NSInteger)index shouldCancel:(BOOL)shouldCancel +{ + if(shouldCancel) + return NO; + return YES; +} + +- (NSInteger)numberOfItemsInMenu:(NSMenu *)menu +{ + return [menu numberOfItems]; +} + +- (void)menuNeedsUpdate:(NSMenu *)menu +{ + _parent->RaiseNeedsUpdate(); +} + + +@end -extern IAvnAppMenu* CreateAppMenu() +extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* cb) { @autoreleasepool { - id menuBar = [NSMenu new]; - return new AvnAppMenu(menuBar); + return new AvnAppMenu(cb); } } -extern IAvnAppMenuItem* CreateAppMenuItem() +extern IAvnMenuItem* CreateAppMenuItem() { @autoreleasepool { @@ -202,7 +306,7 @@ extern IAvnAppMenuItem* CreateAppMenuItem() } } -extern IAvnAppMenuItem* CreateAppMenuItemSeperator() +extern IAvnMenuItem* CreateAppMenuItemSeperator() { @autoreleasepool { @@ -210,10 +314,10 @@ extern IAvnAppMenuItem* CreateAppMenuItemSeperator() } } -static IAvnAppMenu* s_appMenu = nullptr; +static IAvnMenu* s_appMenu = nullptr; static NSMenuItem* s_appMenuItem = nullptr; -extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) +extern void SetAppMenu (NSString* appName, IAvnMenu* menu) { s_appMenu = menu; @@ -294,7 +398,7 @@ extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) } } -extern IAvnAppMenu* GetAppMenu () +extern IAvnMenu* GetAppMenu () { return s_appMenu; } diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index 2d72226faf..f93436d157 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -54,9 +54,11 @@ private: { public: FORWARD_IUNKNOWN() + bool Running = false; bool Cancelled = false; - virtual void Cancel() + + virtual void Cancel() override { Cancelled = true; if(Running) diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 5c85a2f423..505900d584 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -19,7 +19,9 @@ class WindowBaseImpl; -(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) restoreParentWindow; -(bool) shouldTryToHandleEvents; --(void) applyMenu:(NSMenu *)menu; +-(void) showAppMenuOnly; +-(void) showWindowMenuWithAppMenu; +-(void) applyMenu:(NSMenu* _Nullable)menu; -(double) getScaling; @end diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 6298118c10..109ed63e6f 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -27,7 +27,7 @@ public: NSObject* renderTarget; AvnPoint lastPositionSet; NSString* _lastTitle; - IAvnAppMenu* _mainMenu; + IAvnMenu* _mainMenu; bool _shown; WindowBaseImpl(IAvnWindowBaseEvents* events, IAvnGlContext* gl) @@ -234,7 +234,7 @@ public: } } - virtual HRESULT SetMainMenu(IAvnAppMenu* menu) override + virtual HRESULT SetMainMenu(IAvnMenu* menu) override { _mainMenu = menu; @@ -244,18 +244,11 @@ public: [Window applyMenu:nsmenu]; - return S_OK; - } - - virtual HRESULT ObtainMainMenu(IAvnAppMenu** ret) override - { - if(ret == nullptr) + if ([Window isKeyWindow]) { - return E_POINTER; + [Window showWindowMenuWithAppMenu]; } - *ret = _mainMenu; - return S_OK; } @@ -1151,8 +1144,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent ComPtr _parent; bool _canBecomeKeyAndMain; bool _closed; - NSMenu* _menu; - bool _isAppMenuApplied; + AvnMenu* _menu; double _lastScaling; } @@ -1189,32 +1181,64 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } --(void) applyMenu:(NSMenu *)menu +-(void) showWindowMenuWithAppMenu { - if(menu == nullptr) + if(_menu != nullptr) { - menu = [NSMenu new]; + auto appMenuItem = ::GetAppMenuItem(); + + if(appMenuItem != nullptr) + { + auto appMenu = [appMenuItem menu]; + + [appMenu removeItem:appMenuItem]; + + [_menu insertItem:appMenuItem atIndex:0]; + + [_menu setHasGlobalMenuItem:true]; + } + + [NSApp setMenu:_menu]; } +} + +-(void) showAppMenuOnly +{ + auto appMenuItem = ::GetAppMenuItem(); - _menu = menu; - - if ([self isKeyWindow]) + if(appMenuItem != nullptr) { - auto appMenu = ::GetAppMenuItem(); + auto appMenu = ::GetAppMenu(); - if(appMenu != nullptr) + auto nativeAppMenu = dynamic_cast(appMenu); + + [[appMenuItem menu] removeItem:appMenuItem]; + + if(_menu != nullptr) { - [[appMenu menu] removeItem:appMenu]; - - [_menu insertItem:appMenu atIndex:0]; - - _isAppMenuApplied = true; + [_menu setHasGlobalMenuItem:false]; } - [NSApp setMenu:menu]; + [nativeAppMenu->GetNative() addItem:appMenuItem]; + + [NSApp setMenu:nativeAppMenu->GetNative()]; + } + else + { + [NSApp setMenu:nullptr]; } } +-(void) applyMenu:(AvnMenu *)menu +{ + if(menu == nullptr) + { + menu = [AvnMenu new]; + } + + _menu = menu; +} + -(void) setCanBecomeKeyAndMain { _canBecomeKeyAndMain = true; @@ -1315,23 +1339,7 @@ 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]; + [self showWindowMenuWithAppMenu]; _parent->BaseEvents->Activated(); [super becomeKeyWindow]; @@ -1383,26 +1391,7 @@ 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? + [self showAppMenuOnly]; [super resignKeyWindow]; } diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index d25de9c1f5..1f48350448 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -36,6 +36,15 @@ + + + + + + + diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index b40fdb4a17..d97325ef8d 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -29,6 +29,7 @@ namespace ControlCatalog DataContext = new MainWindowViewModel(_notificationArea); _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Menu; + var mainMenu = this.FindControl("MainMenu"); mainMenu.AttachedToVisualTree += MenuAttached; } diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 89e7653618..b6aa3e92cd 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -10,6 +10,8 @@ namespace ControlCatalog.ViewModels { private IManagedNotificationManager _notificationManager; + private bool _isMenuItemChecked = true; + public MainWindowViewModel(IManagedNotificationManager notificationManager) { _notificationManager = notificationManager; @@ -42,6 +44,11 @@ namespace ControlCatalog.ViewModels { (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown(); }); + + ToggleMenuItemCheckedCommand = ReactiveCommand.Create(() => + { + IsMenuItemChecked = !IsMenuItemChecked; + }); } public IManagedNotificationManager NotificationManager @@ -50,6 +57,12 @@ namespace ControlCatalog.ViewModels set { this.RaiseAndSetIfChanged(ref _notificationManager, value); } } + public bool IsMenuItemChecked + { + get { return _isMenuItemChecked; } + set { this.RaiseAndSetIfChanged(ref _isMenuItemChecked, value); } + } + public ReactiveCommand ShowCustomManagedNotificationCommand { get; } public ReactiveCommand ShowManagedNotificationCommand { get; } @@ -59,5 +72,7 @@ namespace ControlCatalog.ViewModels public ReactiveCommand AboutCommand { get; } public ReactiveCommand ExitCommand { get; } + + public ReactiveCommand ToggleMenuItemCheckedCommand { get; } } } diff --git a/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs b/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs new file mode 100644 index 0000000000..672d5c1a13 --- /dev/null +++ b/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs @@ -0,0 +1,7 @@ +namespace Avalonia.Controls +{ + public interface INativeMenuExporterEventsImplBridge + { + void RaiseNeedsUpdate (); + } +} diff --git a/src/Avalonia.Controls/INativeMenuItemExporterEventsImplBridge.cs b/src/Avalonia.Controls/INativeMenuItemExporterEventsImplBridge.cs new file mode 100644 index 0000000000..6cb68d8ddd --- /dev/null +++ b/src/Avalonia.Controls/INativeMenuItemExporterEventsImplBridge.cs @@ -0,0 +1,7 @@ +namespace Avalonia.Controls +{ + public interface INativeMenuItemExporterEventsImplBridge + { + void RaiseClicked (); + } +} diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs index 54aa2b5e3d..38a9f03d29 100644 --- a/src/Avalonia.Controls/NativeMenu.cs +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -3,13 +3,11 @@ 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 + public partial class NativeMenu : AvaloniaObject, IEnumerable, INativeMenuExporterEventsImplBridge { private readonly AvaloniaList _items = new AvaloniaList { ResetBehavior = ResetBehavior.Remove }; @@ -17,12 +15,22 @@ namespace Avalonia.Controls [Content] public IList Items => _items; + /// + /// Raised when the user clicks the menu and before its opened. Use this event to update the menu dynamically. + /// + public event EventHandler Opening; + public NativeMenu() { _items.Validate = Validator; _items.CollectionChanged += ItemsChanged; } + void INativeMenuExporterEventsImplBridge.RaiseNeedsUpdate() + { + Opening?.Invoke(this, EventArgs.Empty); + } + private void Validator(NativeMenuItemBase obj) { if (obj.Parent != null) @@ -31,10 +39,10 @@ namespace Avalonia.Controls private void ItemsChanged(object sender, NotifyCollectionChangedEventArgs e) { - if(e.OldItems!=null) + if (e.OldItems != null) foreach (NativeMenuItemBase i in e.OldItems) i.Parent = null; - if(e.NewItems!=null) + if (e.NewItems != null) foreach (NativeMenuItemBase i in e.NewItems) i.Parent = this; } @@ -49,7 +57,7 @@ namespace Avalonia.Controls } public void Add(NativeMenuItemBase item) => _items.Add(item); - + public IEnumerator GetEnumerator() => _items.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() diff --git a/src/Avalonia.Controls/NativeMenuBar.cs b/src/Avalonia.Controls/NativeMenuBar.cs index 9b96ab9c8c..63bb39108f 100644 --- a/src/Avalonia.Controls/NativeMenuBar.cs +++ b/src/Avalonia.Controls/NativeMenuBar.cs @@ -30,7 +30,7 @@ namespace Avalonia.Controls private static void OnMenuItemClick(object sender, RoutedEventArgs e) { - (((MenuItem)sender).DataContext as NativeMenuItem)?.RaiseClick(); + (((MenuItem)sender).DataContext as INativeMenuItemExporterEventsImplBridge)?.RaiseClicked(); } } } diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index c1144d45b2..6bcf676ddb 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -5,11 +5,13 @@ using Avalonia.Utilities; namespace Avalonia.Controls { - public class NativeMenuItem : NativeMenuItemBase + public class NativeMenuItem : NativeMenuItemBase, INativeMenuItemExporterEventsImplBridge { private string _header; private KeyGesture _gesture; - private bool _enabled = true; + private bool _isEnabled = true; + private ICommand _command; + private bool _isChecked = false; private NativeMenu _menu; @@ -55,13 +57,7 @@ namespace Avalonia.Controls } 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; - }); + AvaloniaProperty.RegisterDirect(nameof(Menu), o => o.Menu, (o, v) => o.Menu = v); public NativeMenu Menu { @@ -75,38 +71,40 @@ namespace Avalonia.Controls } public static readonly DirectProperty HeaderProperty = - AvaloniaProperty.RegisterDirect(nameof(Header), o => o._header, (o, v) => o._header = v); + AvaloniaProperty.RegisterDirect(nameof(Header), o => o.Header, (o, v) => o.Header = v); public string Header { - get => GetValue(HeaderProperty); - set => SetValue(HeaderProperty, value); + get => _header; + set => SetAndRaise(HeaderProperty, ref _header, value); } public static readonly DirectProperty GestureProperty = - AvaloniaProperty.RegisterDirect(nameof(Gesture), o => o._gesture, (o, v) => o._gesture = v); + AvaloniaProperty.RegisterDirect(nameof(Gesture), o => o.Gesture, (o, v) => o.Gesture = v); public KeyGesture Gesture { - get => GetValue(GestureProperty); - set => SetValue(GestureProperty, value); + get => _gesture; + set => SetAndRaise(GestureProperty, ref _gesture, value); } - private ICommand _command; + public static readonly DirectProperty IsCheckedProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsChecked), + o => o.IsChecked, + (o, v) => o.IsChecked = v); + + public bool IsChecked + { + get => _isChecked; + set => SetAndRaise(IsCheckedProperty, ref _isChecked, value); + } 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(); - }); + Button.CommandProperty.AddOwner( + menuItem => menuItem.Command, + (menuItem, command) => menuItem.Command = command, + enableDataValidation: true); /// /// Defines the property. @@ -114,27 +112,39 @@ namespace Avalonia.Controls public static readonly StyledProperty CommandParameterProperty = Button.CommandParameterProperty.AddOwner(); - public static readonly DirectProperty EnabledProperty = - AvaloniaProperty.RegisterDirect(nameof(Enabled), o => o._enabled, - (o, v) => o._enabled = v, true); + public static readonly DirectProperty IsEnabledProperty = + AvaloniaProperty.RegisterDirect(nameof(IsEnabled), o => o.IsEnabled, (o, v) => o.IsEnabled = v, true); - public bool Enabled + public bool IsEnabled { - get => GetValue(EnabledProperty); - set => SetValue(EnabledProperty, value); + get => _isEnabled; + set => SetAndRaise(IsEnabledProperty, ref _isEnabled, value); } void CanExecuteChanged() { - Enabled = _command?.CanExecute(null) ?? true; + IsEnabled = _command?.CanExecute(null) ?? true; } public bool HasClickHandlers => Clicked != null; public ICommand Command { - get => GetValue(CommandProperty); - set => SetValue(CommandProperty, value); + get => _command; + set + { + if (_command != null) + WeakSubscriptionManager.Unsubscribe(_command, + nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber); + + SetAndRaise(CommandProperty, ref _command, value); + + if (_command != null) + WeakSubscriptionManager.Subscribe(_command, + nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber); + + CanExecuteChanged(); + } } /// @@ -149,7 +159,7 @@ namespace Avalonia.Controls public event EventHandler Clicked; - public void RaiseClick() + void INativeMenuItemExporterEventsImplBridge.RaiseClicked() { Clicked?.Invoke(this, new EventArgs()); diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 90239b5a49..cd56342059 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -210,7 +210,7 @@ namespace Avalonia.FreeDesktop return null; if (item.Menu != null && item.Menu.Items.Count == 0) return false; - if (item.Enabled == false) + if (item.IsEnabled == false) return false; return null; } @@ -319,10 +319,10 @@ namespace Avalonia.FreeDesktop { var item = GetMenu(id).item; - if (item is NativeMenuItem menuItem) + if (item is NativeMenuItem menuItem && item is INativeMenuItemExporterEventsImplBridge bridge) { - if (menuItem?.Enabled == true) - menuItem.RaiseClick(); + if (menuItem?.IsEnabled == true) + bridge?.RaiseClicked(); } } } diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 95c2aabb3d..0f2551ffeb 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -1,185 +1,21 @@ using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using System.Text; -using Avalonia.Collections; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Platform; -using Avalonia.Input; +using Avalonia.Dialogs; using Avalonia.Native.Interop; -using Avalonia.Platform.Interop; using Avalonia.Threading; -using Avalonia.Dialogs; -using Avalonia.Controls.ApplicationLifetimes; namespace Avalonia.Native { - enum OsxUnicodeSpecialKey - { - NSUpArrowFunctionKey = 0xF700, - NSDownArrowFunctionKey = 0xF701, - NSLeftArrowFunctionKey = 0xF702, - NSRightArrowFunctionKey = 0xF703, - NSF1FunctionKey = 0xF704, - NSF2FunctionKey = 0xF705, - NSF3FunctionKey = 0xF706, - NSF4FunctionKey = 0xF707, - NSF5FunctionKey = 0xF708, - NSF6FunctionKey = 0xF709, - NSF7FunctionKey = 0xF70A, - NSF8FunctionKey = 0xF70B, - NSF9FunctionKey = 0xF70C, - NSF10FunctionKey = 0xF70D, - NSF11FunctionKey = 0xF70E, - NSF12FunctionKey = 0xF70F, - NSF13FunctionKey = 0xF710, - NSF14FunctionKey = 0xF711, - NSF15FunctionKey = 0xF712, - NSF16FunctionKey = 0xF713, - NSF17FunctionKey = 0xF714, - NSF18FunctionKey = 0xF715, - NSF19FunctionKey = 0xF716, - NSF20FunctionKey = 0xF717, - NSF21FunctionKey = 0xF718, - NSF22FunctionKey = 0xF719, - NSF23FunctionKey = 0xF71A, - NSF24FunctionKey = 0xF71B, - NSF25FunctionKey = 0xF71C, - NSF26FunctionKey = 0xF71D, - NSF27FunctionKey = 0xF71E, - NSF28FunctionKey = 0xF71F, - NSF29FunctionKey = 0xF720, - NSF30FunctionKey = 0xF721, - NSF31FunctionKey = 0xF722, - NSF32FunctionKey = 0xF723, - NSF33FunctionKey = 0xF724, - NSF34FunctionKey = 0xF725, - NSF35FunctionKey = 0xF726, - NSInsertFunctionKey = 0xF727, - NSDeleteFunctionKey = 0xF728, - NSHomeFunctionKey = 0xF729, - NSBeginFunctionKey = 0xF72A, - NSEndFunctionKey = 0xF72B, - NSPageUpFunctionKey = 0xF72C, - NSPageDownFunctionKey = 0xF72D, - NSPrintScreenFunctionKey = 0xF72E, - NSScrollLockFunctionKey = 0xF72F, - NSPauseFunctionKey = 0xF730, - NSSysReqFunctionKey = 0xF731, - NSBreakFunctionKey = 0xF732, - NSResetFunctionKey = 0xF733, - NSStopFunctionKey = 0xF734, - NSMenuFunctionKey = 0xF735, - NSUserFunctionKey = 0xF736, - NSSystemFunctionKey = 0xF737, - NSPrintFunctionKey = 0xF738, - NSClearLineFunctionKey = 0xF739, - NSClearDisplayFunctionKey = 0xF73A, - NSInsertLineFunctionKey = 0xF73B, - NSDeleteLineFunctionKey = 0xF73C, - NSInsertCharFunctionKey = 0xF73D, - NSDeleteCharFunctionKey = 0xF73E, - NSPrevFunctionKey = 0xF73F, - NSNextFunctionKey = 0xF740, - NSSelectFunctionKey = 0xF741, - NSExecuteFunctionKey = 0xF742, - NSUndoFunctionKey = 0xF743, - NSRedoFunctionKey = 0xF744, - NSFindFunctionKey = 0xF745, - NSHelpFunctionKey = 0xF746, - NSModeSwitchFunctionKey = 0xF747 - } - - public class MenuActionCallback : CallbackBase, IAvnActionCallback - { - private Action _action; - - public MenuActionCallback(Action action) - { - _action = action; - } - - void IAvnActionCallback.Run() - { - _action?.Invoke(); - } - } - - public class PredicateCallback : CallbackBase, IAvnPredicateCallback - { - private Func _predicate; - - public PredicateCallback(Func predicate) - { - _predicate = predicate; - } - - bool IAvnPredicateCallback.Evaluate() - { - return _predicate(); - } - } - class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter { private IAvaloniaNativeFactory _factory; - private NativeMenu _menu; private bool _resetQueued; private bool _exported = false; private IAvnWindow _nativeWindow; - private List _menuItems = new List(); - - private static Dictionary osxKeys = new Dictionary - { - {Key.Up, OsxUnicodeSpecialKey.NSUpArrowFunctionKey }, - {Key.Down, OsxUnicodeSpecialKey.NSDownArrowFunctionKey }, - {Key.Left, OsxUnicodeSpecialKey.NSLeftArrowFunctionKey }, - {Key.Right, OsxUnicodeSpecialKey.NSRightArrowFunctionKey }, - { Key.F1, OsxUnicodeSpecialKey.NSF1FunctionKey }, - { Key.F2, OsxUnicodeSpecialKey.NSF2FunctionKey }, - { Key.F3, OsxUnicodeSpecialKey.NSF3FunctionKey }, - { Key.F4, OsxUnicodeSpecialKey.NSF4FunctionKey }, - { Key.F5, OsxUnicodeSpecialKey.NSF5FunctionKey }, - { Key.F6, OsxUnicodeSpecialKey.NSF6FunctionKey }, - { Key.F7, OsxUnicodeSpecialKey.NSF7FunctionKey }, - { Key.F8, OsxUnicodeSpecialKey.NSF8FunctionKey }, - { Key.F9, OsxUnicodeSpecialKey.NSF9FunctionKey }, - { Key.F10, OsxUnicodeSpecialKey.NSF10FunctionKey }, - { Key.F11, OsxUnicodeSpecialKey.NSF11FunctionKey }, - { Key.F12, OsxUnicodeSpecialKey.NSF12FunctionKey }, - { Key.F13, OsxUnicodeSpecialKey.NSF13FunctionKey }, - { Key.F14, OsxUnicodeSpecialKey.NSF14FunctionKey }, - { Key.F15, OsxUnicodeSpecialKey.NSF15FunctionKey }, - { Key.F16, OsxUnicodeSpecialKey.NSF16FunctionKey }, - { Key.F17, OsxUnicodeSpecialKey.NSF17FunctionKey }, - { Key.F18, OsxUnicodeSpecialKey.NSF18FunctionKey }, - { Key.F19, OsxUnicodeSpecialKey.NSF19FunctionKey }, - { Key.F20, OsxUnicodeSpecialKey.NSF20FunctionKey }, - { Key.F21, OsxUnicodeSpecialKey.NSF21FunctionKey }, - { Key.F22, OsxUnicodeSpecialKey.NSF22FunctionKey }, - { Key.F23, OsxUnicodeSpecialKey.NSF23FunctionKey }, - { Key.F24, OsxUnicodeSpecialKey.NSF24FunctionKey }, - { Key.Insert, OsxUnicodeSpecialKey.NSInsertFunctionKey }, - { Key.Delete, OsxUnicodeSpecialKey.NSDeleteFunctionKey }, - { Key.Home, OsxUnicodeSpecialKey.NSHomeFunctionKey }, - //{ Key.Begin, OsxUnicodeSpecialKey.NSBeginFunctionKey }, - { Key.End, OsxUnicodeSpecialKey.NSEndFunctionKey }, - { Key.PageUp, OsxUnicodeSpecialKey.NSPageUpFunctionKey }, - { Key.PageDown, OsxUnicodeSpecialKey.NSPageDownFunctionKey }, - { Key.PrintScreen, OsxUnicodeSpecialKey.NSPrintScreenFunctionKey }, - { Key.Scroll, OsxUnicodeSpecialKey.NSScrollLockFunctionKey }, - //{ Key.SysReq, OsxUnicodeSpecialKey.NSSysReqFunctionKey }, - //{ Key.Break, OsxUnicodeSpecialKey.NSBreakFunctionKey }, - //{ Key.Reset, OsxUnicodeSpecialKey.NSResetFunctionKey }, - //{ Key.Stop, OsxUnicodeSpecialKey.NSStopFunctionKey }, - //{ Key.Menu, OsxUnicodeSpecialKey.NSMenuFunctionKey }, - //{ Key.UserFunction, OsxUnicodeSpecialKey.NSUserFunctionKey }, - //{ Key.SystemFunction, OsxUnicodeSpecialKey.NSSystemFunctionKey }, - { Key.Print, OsxUnicodeSpecialKey.NSPrintFunctionKey }, - //{ Key.ClearLine, OsxUnicodeSpecialKey.NSClearLineFunctionKey }, - //{ Key.ClearDisplay, OsxUnicodeSpecialKey.NSClearDisplayFunctionKey }, - }; + private NativeMenu _menu; + private IAvnMenu _nativeMenu; public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) { @@ -193,7 +29,6 @@ namespace Avalonia.Native { _factory = factory; - _menu = NativeMenu.GetMenu(Application.Current); DoLayoutReset(); } @@ -203,17 +38,19 @@ namespace Avalonia.Native 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; + _menu = menu == null ? new NativeMenu() : menu; DoLayoutReset(); } + internal void UpdateIfNeeded() + { + if (_resetQueued) + { + DoLayoutReset(); + } + } + private static NativeMenu CreateDefaultAppMenu() { var result = new NativeMenu(); @@ -237,50 +74,34 @@ namespace Avalonia.Native return result; } - 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) + if (_nativeWindow is null) { - _menu = NativeMenu.GetMenu(Application.Current); + var appMenu = NativeMenu.GetMenu(Application.Current); - if(_menu != null) - { - SetMenu(_menu); - } - else + if (appMenu == null) { - SetMenu(CreateDefaultAppMenu()); + appMenu = CreateDefaultAppMenu(); + NativeMenu.SetMenu(Application.Current, appMenu); } + + SetMenu(appMenu); } else { - SetMenu(_nativeWindow, _menu?.Items); + if (_menu != null) + { + SetMenu(_nativeWindow, _menu); + } } _exported = true; } - private void QueueReset() + internal void QueueReset() { if (_resetQueued) return; @@ -288,188 +109,64 @@ namespace Avalonia.Native Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); } - private IAvnAppMenu CreateSubmenu(ICollection children) + private void SetMenu(NativeMenu menu) { - var menu = _factory.CreateMenu(); - - SetChildren(menu, children); + var menuItem = menu.Parent; - return menu; - } + var appMenuHolder = menuItem?.Parent; - private void AddMenuItem(NativeMenuItem item) - { - if (item.Menu?.Items != null) + if (menu.Parent is null) { - ((INotifyCollectionChanged)item.Menu.Items).CollectionChanged += OnMenuItemsChanged; + menuItem = new NativeMenuItem(); } - } - private static string ConvertOSXSpecialKeyCodes(Key key) - { - if (osxKeys.ContainsKey(key)) - { - return ((char)osxKeys[key]).ToString(); - } - else + if (appMenuHolder is null) { - return key.ToString().ToLower(); - } - } + appMenuHolder = new NativeMenu(); - private void SetChildren(IAvnAppMenu menu, ICollection children) - { - foreach (var i in children) - { - if (i is NativeMenuItem item) - { - AddMenuItem(item); - - var menuItem = _factory.CreateMenuItem(); - - using (var buffer = new Utf8Buffer(item.Header)) - { - menuItem.Title = buffer.DangerousGetHandle(); - } - - if (item.Gesture != null) - { - using (var buffer = new Utf8Buffer(ConvertOSXSpecialKeyCodes(item.Gesture.Key))) - { - 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()); - } + appMenuHolder.Add(menuItem); } - } - private void AddItemsToMenu(IAvnAppMenu menu, ICollection items, bool isMainMenu = false) - { - foreach (var i in items) - { - if (i is NativeMenuItem item) - { - var menuItem = _factory.CreateMenuItem(); - - AddMenuItem(item); - - menuItem.SetAction(new PredicateCallback(() => - { - if (item.Command != null || item.HasClickHandlers) - { - return item.Enabled; - } - - return false; - }), new MenuActionCallback(() => { item.RaiseClick(); })); - - if (item.Menu?.Items.Count >= 0 || isMainMenu) - { - var subMenu = CreateSubmenu(item.Menu?.Items); - - menuItem.SetSubMenu(subMenu); - - using (var buffer = new Utf8Buffer(item.Header)) - { - subMenu.Title = buffer.DangerousGetHandle(); - } - } - else - { - using (var buffer = new Utf8Buffer(item.Header)) - { - menuItem.Title = buffer.DangerousGetHandle(); - } - - if (item.Gesture != null) - { - using (var buffer = new Utf8Buffer(item.Gesture.Key.ToString().ToLower())) - { - menuItem.SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); - } - } - } - - menu.AddItem(menuItem); - } - else if(i is NativeMenuItemSeperator seperator) - { - menu.AddItem(_factory.CreateMenuItemSeperator()); - } - } - } + menuItem.Menu = menu; - private void SetMenu(NativeMenu menu) - { - var appMenu = _factory.ObtainAppMenu(); + var setMenu = false; - if (appMenu is null) + if (_nativeMenu is null) { - appMenu = _factory.CreateMenu(); - } + _nativeMenu = IAvnMenu.Create(_factory); - var menuItem = menu.Parent; + _nativeMenu.Initialise(this, appMenuHolder, ""); - if(menu.Parent is null) - { - menuItem = new NativeMenuItem(); + setMenu = true; } - menuItem.Menu = menu; - - appMenu.Clear(); - AddItemsToMenu(appMenu, new List { menuItem }); + _nativeMenu.Update(_factory, appMenuHolder); - _factory.SetAppMenu(appMenu); + if (setMenu) + { + _factory.SetAppMenu(_nativeMenu); + } } - private void SetMenu(IAvnWindow avnWindow, ICollection menuItems) + private void SetMenu(IAvnWindow avnWindow, NativeMenu menu) { - if (menuItems is null) + var setMenu = false; + + if (_nativeMenu is null) { - menuItems = new List(); - } + _nativeMenu = IAvnMenu.Create(_factory); - var appMenu = avnWindow.ObtainMainMenu(); + _nativeMenu.Initialise(this, menu, ""); - if (appMenu is null) - { - appMenu = _factory.CreateMenu(); + setMenu = true; } - appMenu.Clear(); - AddItemsToMenu(appMenu, menuItems); + _nativeMenu.Update(_factory, menu); - avnWindow.SetMainMenu(appMenu); + if(setMenu) + { + avnWindow.SetMainMenu(_nativeMenu); + } } } } diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs new file mode 100644 index 0000000000..8a49559a02 --- /dev/null +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Reactive.Disposables; +using Avalonia.Controls; +using Avalonia.Platform.Interop; + +namespace Avalonia.Native.Interop +{ + class MenuEvents : CallbackBase, IAvnMenuEvents + { + private IAvnMenu _parent; + + public void Initialise(IAvnMenu parent) + { + _parent = parent; + } + + public void NeedsUpdate() + { + _parent?.RaiseNeedsUpdate(); + } + } + + public partial class IAvnMenu + { + private MenuEvents _events; + private AvaloniaNativeMenuExporter _exporter; + private List _menuItems = new List(); + private Dictionary _menuItemLookup = new Dictionary(); + private CompositeDisposable _propertyDisposables = new CompositeDisposable(); + + internal void RaiseNeedsUpdate() + { + (ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseNeedsUpdate(); + + _exporter.UpdateIfNeeded(); + } + + internal NativeMenu ManagedMenu { get; private set; } + + public static IAvnMenu Create(IAvaloniaNativeFactory factory) + { + var events = new MenuEvents(); + + var menu = factory.CreateMenu(events); + + events.Initialise(menu); + + menu._events = events; + + return menu; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _events.Dispose(); + } + } + + private void RemoveAndDispose(IAvnMenuItem item) + { + _menuItemLookup.Remove(item.ManagedMenuItem); + _menuItems.Remove(item); + RemoveItem(item); + + item.Deinitialise(); + item.Dispose(); + } + + private void MoveExistingTo(int index, IAvnMenuItem item) + { + _menuItems.Remove(item); + _menuItems.Insert(index, item); + + RemoveItem(item); + InsertItem(index, item); + } + + private IAvnMenuItem CreateNewAt(IAvaloniaNativeFactory factory, int index, NativeMenuItemBase item) + { + var result = CreateNew(factory, item); + + result.Initialise(item); + + _menuItemLookup.Add(result.ManagedMenuItem, result); + _menuItems.Insert(index, result); + + InsertItem(index, result); + + return result; + } + + private IAvnMenuItem CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item) + { + var nativeItem = item is NativeMenuItemSeperator ? factory.CreateMenuItemSeperator() : factory.CreateMenuItem(); + nativeItem.ManagedMenuItem = item; + + return nativeItem; + } + + internal void Initialise(AvaloniaNativeMenuExporter exporter, NativeMenu managedMenu, string title) + { + _exporter = exporter; + ManagedMenu = managedMenu; + + ((INotifyCollectionChanged)ManagedMenu.Items).CollectionChanged += OnMenuItemsChanged; + + if (!string.IsNullOrWhiteSpace(title)) + { + using (var buffer = new Utf8Buffer(title)) + { + Title = buffer.DangerousGetHandle(); + } + } + } + + internal void Deinitialise() + { + ((INotifyCollectionChanged)ManagedMenu.Items).CollectionChanged -= OnMenuItemsChanged; + + foreach (var item in _menuItems) + { + item.Deinitialise(); + item.Dispose(); + } + } + + internal void Update(IAvaloniaNativeFactory factory, NativeMenu menu) + { + if (menu != ManagedMenu) + { + throw new ArgumentException("The menu being updated does not match.", nameof(menu)); + } + + for (int i = 0; i < menu.Items.Count; i++) + { + IAvnMenuItem nativeItem; + + if (i >= _menuItems.Count) + { + nativeItem = CreateNewAt(factory, i, menu.Items[i]); + } + else if (menu.Items[i] == _menuItems[i].ManagedMenuItem) + { + nativeItem = _menuItems[i]; + } + else if (_menuItemLookup.TryGetValue(menu.Items[i], out nativeItem)) + { + MoveExistingTo(i, nativeItem); + } + else + { + nativeItem = CreateNewAt(factory, i, menu.Items[i]); + } + + if (menu.Items[i] is NativeMenuItem nmi) + { + nativeItem.Update(_exporter, factory, nmi); + } + } + + while (_menuItems.Count > menu.Items.Count) + { + RemoveAndDispose(_menuItems[_menuItems.Count - 1]); + } + } + + private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + _exporter.QueueReset(); + } + } +} diff --git a/src/Avalonia.Native/IAvnMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs new file mode 100644 index 0000000000..a9730920f1 --- /dev/null +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -0,0 +1,136 @@ +using System; +using System.Reactive.Disposables; +using Avalonia.Controls; +using Avalonia.Platform.Interop; + +namespace Avalonia.Native.Interop +{ + public partial class IAvnMenuItem + { + private IAvnMenu _subMenu; + private CompositeDisposable _propertyDisposables = new CompositeDisposable(); + private IDisposable _currentActionDisposable; + + public NativeMenuItemBase ManagedMenuItem { get; set; } + + private void UpdateTitle(string title) + { + using (var buffer = new Utf8Buffer(string.IsNullOrWhiteSpace(title) ? "" : title)) + { + Title = buffer.DangerousGetHandle(); + } + } + + private void UpdateIsChecked(bool isChecked) + { + IsChecked = isChecked; + } + + private void UpdateGesture(Input.KeyGesture gesture) + { + // todo ensure backend can cope with setting null gesture. + using (var buffer = new Utf8Buffer(gesture == null ? "" : OsxUnicodeKeys.ConvertOSXSpecialKeyCodes(gesture.Key))) + { + var modifiers = gesture == null ? AvnInputModifiers.AvnInputModifiersNone : (AvnInputModifiers)gesture.KeyModifiers; + SetGesture(buffer.DangerousGetHandle(), modifiers); + } + } + + private void UpdateAction(NativeMenuItem item) + { + _currentActionDisposable?.Dispose(); + + var action = new PredicateCallback(() => + { + if (item.Command != null || item.HasClickHandlers) + { + return item.IsEnabled; + } + + return false; + }); + + var callback = new MenuActionCallback(() => { (item as INativeMenuItemExporterEventsImplBridge)?.RaiseClicked(); }); + + _currentActionDisposable = Disposable.Create(() => + { + action.Dispose(); + callback.Dispose(); + }); + + SetAction(action, callback); + } + + internal void Initialise(NativeMenuItemBase nativeMenuItem) + { + ManagedMenuItem = nativeMenuItem; + + if (ManagedMenuItem is NativeMenuItem item) + { + UpdateTitle(item.Header); + + UpdateGesture(item.Gesture); + + UpdateAction(ManagedMenuItem as NativeMenuItem); + + UpdateIsChecked(item.IsChecked); + + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.HeaderProperty) + .Subscribe(x => UpdateTitle(x))); + + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.GestureProperty) + .Subscribe(x => UpdateGesture(x))); + + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.CommandProperty) + .Subscribe(x => UpdateAction(ManagedMenuItem as NativeMenuItem))); + + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.IsCheckedProperty) + .Subscribe(x => UpdateIsChecked(x))); + } + } + + internal void Deinitialise() + { + if (_subMenu != null) + { + SetSubMenu(null); + _subMenu.Deinitialise(); + _subMenu.Dispose(); + _subMenu = null; + } + + _propertyDisposables?.Dispose(); + _currentActionDisposable?.Dispose(); + } + + internal void Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenuItem item) + { + if (item != ManagedMenuItem) + { + throw new ArgumentException("The item does not match the menuitem being updated.", nameof(item)); + } + + if (item.Menu != null) + { + if (_subMenu == null) + { + _subMenu = IAvnMenu.Create(factory); + + _subMenu.Initialise(exporter, item.Menu, item.Header); + + SetSubMenu(_subMenu); + } + + _subMenu.Update(factory, item.Menu); + } + + if (item.Menu == null && _subMenu != null) + { + _subMenu.Deinitialise(); + _subMenu.Dispose(); + + SetSubMenu(null); + } + } + } +} diff --git a/src/Avalonia.Native/Mappings.xml b/src/Avalonia.Native/Mappings.xml index 7ac6377f78..fcaa31a249 100644 --- a/src/Avalonia.Native/Mappings.xml +++ b/src/Avalonia.Native/Mappings.xml @@ -19,5 +19,7 @@ + + diff --git a/src/Avalonia.Native/MenuActionCallback.cs b/src/Avalonia.Native/MenuActionCallback.cs new file mode 100644 index 0000000000..5318195f30 --- /dev/null +++ b/src/Avalonia.Native/MenuActionCallback.cs @@ -0,0 +1,20 @@ +using System; +using Avalonia.Native.Interop; + +namespace Avalonia.Native +{ + public class MenuActionCallback : CallbackBase, IAvnActionCallback + { + private Action _action; + + public MenuActionCallback(Action action) + { + _action = action; + } + + void IAvnActionCallback.Run() + { + _action?.Invoke(); + } + } +} diff --git a/src/Avalonia.Native/OsxUnicodeKeys.cs b/src/Avalonia.Native/OsxUnicodeKeys.cs new file mode 100644 index 0000000000..b4056c8cd8 --- /dev/null +++ b/src/Avalonia.Native/OsxUnicodeKeys.cs @@ -0,0 +1,147 @@ +using System.Collections.Generic; +using Avalonia.Input; + +namespace Avalonia.Native.Interop +{ + internal static class OsxUnicodeKeys + { + enum OsxUnicodeSpecialKey + { + NSUpArrowFunctionKey = 0xF700, + NSDownArrowFunctionKey = 0xF701, + NSLeftArrowFunctionKey = 0xF702, + NSRightArrowFunctionKey = 0xF703, + NSF1FunctionKey = 0xF704, + NSF2FunctionKey = 0xF705, + NSF3FunctionKey = 0xF706, + NSF4FunctionKey = 0xF707, + NSF5FunctionKey = 0xF708, + NSF6FunctionKey = 0xF709, + NSF7FunctionKey = 0xF70A, + NSF8FunctionKey = 0xF70B, + NSF9FunctionKey = 0xF70C, + NSF10FunctionKey = 0xF70D, + NSF11FunctionKey = 0xF70E, + NSF12FunctionKey = 0xF70F, + NSF13FunctionKey = 0xF710, + NSF14FunctionKey = 0xF711, + NSF15FunctionKey = 0xF712, + NSF16FunctionKey = 0xF713, + NSF17FunctionKey = 0xF714, + NSF18FunctionKey = 0xF715, + NSF19FunctionKey = 0xF716, + NSF20FunctionKey = 0xF717, + NSF21FunctionKey = 0xF718, + NSF22FunctionKey = 0xF719, + NSF23FunctionKey = 0xF71A, + NSF24FunctionKey = 0xF71B, + NSF25FunctionKey = 0xF71C, + NSF26FunctionKey = 0xF71D, + NSF27FunctionKey = 0xF71E, + NSF28FunctionKey = 0xF71F, + NSF29FunctionKey = 0xF720, + NSF30FunctionKey = 0xF721, + NSF31FunctionKey = 0xF722, + NSF32FunctionKey = 0xF723, + NSF33FunctionKey = 0xF724, + NSF34FunctionKey = 0xF725, + NSF35FunctionKey = 0xF726, + NSInsertFunctionKey = 0xF727, + NSDeleteFunctionKey = 0xF728, + NSHomeFunctionKey = 0xF729, + NSBeginFunctionKey = 0xF72A, + NSEndFunctionKey = 0xF72B, + NSPageUpFunctionKey = 0xF72C, + NSPageDownFunctionKey = 0xF72D, + NSPrintScreenFunctionKey = 0xF72E, + NSScrollLockFunctionKey = 0xF72F, + NSPauseFunctionKey = 0xF730, + NSSysReqFunctionKey = 0xF731, + NSBreakFunctionKey = 0xF732, + NSResetFunctionKey = 0xF733, + NSStopFunctionKey = 0xF734, + NSMenuFunctionKey = 0xF735, + NSUserFunctionKey = 0xF736, + NSSystemFunctionKey = 0xF737, + NSPrintFunctionKey = 0xF738, + NSClearLineFunctionKey = 0xF739, + NSClearDisplayFunctionKey = 0xF73A, + NSInsertLineFunctionKey = 0xF73B, + NSDeleteLineFunctionKey = 0xF73C, + NSInsertCharFunctionKey = 0xF73D, + NSDeleteCharFunctionKey = 0xF73E, + NSPrevFunctionKey = 0xF73F, + NSNextFunctionKey = 0xF740, + NSSelectFunctionKey = 0xF741, + NSExecuteFunctionKey = 0xF742, + NSUndoFunctionKey = 0xF743, + NSRedoFunctionKey = 0xF744, + NSFindFunctionKey = 0xF745, + NSHelpFunctionKey = 0xF746, + NSModeSwitchFunctionKey = 0xF747 + } + + private static Dictionary s_osxKeys = new Dictionary + { + {Key.Up, OsxUnicodeSpecialKey.NSUpArrowFunctionKey }, + {Key.Down, OsxUnicodeSpecialKey.NSDownArrowFunctionKey }, + {Key.Left, OsxUnicodeSpecialKey.NSLeftArrowFunctionKey }, + {Key.Right, OsxUnicodeSpecialKey.NSRightArrowFunctionKey }, + { Key.F1, OsxUnicodeSpecialKey.NSF1FunctionKey }, + { Key.F2, OsxUnicodeSpecialKey.NSF2FunctionKey }, + { Key.F3, OsxUnicodeSpecialKey.NSF3FunctionKey }, + { Key.F4, OsxUnicodeSpecialKey.NSF4FunctionKey }, + { Key.F5, OsxUnicodeSpecialKey.NSF5FunctionKey }, + { Key.F6, OsxUnicodeSpecialKey.NSF6FunctionKey }, + { Key.F7, OsxUnicodeSpecialKey.NSF7FunctionKey }, + { Key.F8, OsxUnicodeSpecialKey.NSF8FunctionKey }, + { Key.F9, OsxUnicodeSpecialKey.NSF9FunctionKey }, + { Key.F10, OsxUnicodeSpecialKey.NSF10FunctionKey }, + { Key.F11, OsxUnicodeSpecialKey.NSF11FunctionKey }, + { Key.F12, OsxUnicodeSpecialKey.NSF12FunctionKey }, + { Key.F13, OsxUnicodeSpecialKey.NSF13FunctionKey }, + { Key.F14, OsxUnicodeSpecialKey.NSF14FunctionKey }, + { Key.F15, OsxUnicodeSpecialKey.NSF15FunctionKey }, + { Key.F16, OsxUnicodeSpecialKey.NSF16FunctionKey }, + { Key.F17, OsxUnicodeSpecialKey.NSF17FunctionKey }, + { Key.F18, OsxUnicodeSpecialKey.NSF18FunctionKey }, + { Key.F19, OsxUnicodeSpecialKey.NSF19FunctionKey }, + { Key.F20, OsxUnicodeSpecialKey.NSF20FunctionKey }, + { Key.F21, OsxUnicodeSpecialKey.NSF21FunctionKey }, + { Key.F22, OsxUnicodeSpecialKey.NSF22FunctionKey }, + { Key.F23, OsxUnicodeSpecialKey.NSF23FunctionKey }, + { Key.F24, OsxUnicodeSpecialKey.NSF24FunctionKey }, + { Key.Insert, OsxUnicodeSpecialKey.NSInsertFunctionKey }, + { Key.Delete, OsxUnicodeSpecialKey.NSDeleteFunctionKey }, + { Key.Home, OsxUnicodeSpecialKey.NSHomeFunctionKey }, + //{ Key.Begin, OsxUnicodeSpecialKey.NSBeginFunctionKey }, + { Key.End, OsxUnicodeSpecialKey.NSEndFunctionKey }, + { Key.PageUp, OsxUnicodeSpecialKey.NSPageUpFunctionKey }, + { Key.PageDown, OsxUnicodeSpecialKey.NSPageDownFunctionKey }, + { Key.PrintScreen, OsxUnicodeSpecialKey.NSPrintScreenFunctionKey }, + { Key.Scroll, OsxUnicodeSpecialKey.NSScrollLockFunctionKey }, + //{ Key.SysReq, OsxUnicodeSpecialKey.NSSysReqFunctionKey }, + //{ Key.Break, OsxUnicodeSpecialKey.NSBreakFunctionKey }, + //{ Key.Reset, OsxUnicodeSpecialKey.NSResetFunctionKey }, + //{ Key.Stop, OsxUnicodeSpecialKey.NSStopFunctionKey }, + //{ Key.Menu, OsxUnicodeSpecialKey.NSMenuFunctionKey }, + //{ Key.UserFunction, OsxUnicodeSpecialKey.NSUserFunctionKey }, + //{ Key.SystemFunction, OsxUnicodeSpecialKey.NSSystemFunctionKey }, + { Key.Print, OsxUnicodeSpecialKey.NSPrintFunctionKey }, + //{ Key.ClearLine, OsxUnicodeSpecialKey.NSClearLineFunctionKey }, + //{ Key.ClearDisplay, OsxUnicodeSpecialKey.NSClearDisplayFunctionKey }, + }; + + public static string ConvertOSXSpecialKeyCodes(Key key) + { + if (s_osxKeys.ContainsKey(key)) + { + return ((char)s_osxKeys[key]).ToString(); + } + else + { + return key.ToString().ToLower(); + } + } + } +} diff --git a/src/Avalonia.Native/PredicateCallback.cs b/src/Avalonia.Native/PredicateCallback.cs new file mode 100644 index 0000000000..1ed2ae36af --- /dev/null +++ b/src/Avalonia.Native/PredicateCallback.cs @@ -0,0 +1,20 @@ +using System; +using Avalonia.Native.Interop; + +namespace Avalonia.Native +{ + public class PredicateCallback : CallbackBase, IAvnPredicateCallback + { + private Func _predicate; + + public PredicateCallback(Func predicate) + { + _predicate = predicate; + } + + bool IAvnPredicateCallback.Evaluate() + { + return _predicate(); + } + } +}