diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 3475eff654..b10db08adc 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -1,5 +1,6 @@ #include "com.h" #include "key.h" +#include "stddef.h" #define AVNCOM(name, id) COMINTERFACE(name, 2e2cda0a, 9ae5, 4f1b, 8e, 20, 08, 1a, 04, 27, 9f, id) @@ -19,8 +20,9 @@ struct IAvnGlContext; struct IAvnGlDisplay; struct IAvnGlSurfaceRenderTarget; struct IAvnGlSurfaceRenderingSession; -struct IAvnAppMenu; -struct IAvnAppMenuItem; +struct IAvnMenu; +struct IAvnMenuItem; +struct IAvnMenuEvents; enum SystemDecorations { SystemDecorationsNone = 0, @@ -175,6 +177,13 @@ enum AvnWindowEdge WindowEdgeSouthEast }; +enum AvnMenuItemToggleType +{ + None, + CheckMark, + Radio +}; + AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown { public: @@ -188,11 +197,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 +230,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 +395,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 +408,23 @@ 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; + virtual HRESULT SetToggleType (AvnMenuItemToggleType toggleType) = 0; + virtual HRESULT SetIcon (void* data, size_t length) = 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..bfbc6801f8 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,13 +25,14 @@ 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 IAvnActionCallback* _callback; IAvnPredicateCallback* _predicate; bool _isSeperator; + bool _isCheckable; public: FORWARD_IUNKNOWN() @@ -38,7 +41,7 @@ public: NSMenuItem* GetNative(); - virtual HRESULT SetSubMenu (IAvnAppMenu* menu) override; + virtual HRESULT SetSubMenu (IAvnMenu* menu) override; virtual HRESULT SetTitle (void* utf8String) override; @@ -46,29 +49,36 @@ public: virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) override; + virtual HRESULT SetIsChecked (bool isChecked) override; + + virtual HRESULT SetToggleType (AvnMenuItemToggleType toggleType) override; + + virtual HRESULT SetIcon (void* data, size_t length) 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 InsertItem (int index, IAvnMenuItem* item) override; - virtual HRESULT RemoveItem (IAvnAppMenuItem* item) override; + virtual HRESULT RemoveItem (IAvnMenuItem* item) override; virtual HRESULT SetTitle (void* utf8String) override; @@ -76,5 +86,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..dc1245cd23 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 @@ -46,6 +70,7 @@ AvnAppMenuItem::AvnAppMenuItem(bool isSeperator) { + _isCheckable = false; _isSeperator = isSeperator; if(isSeperator) @@ -65,49 +90,134 @@ 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 && _isCheckable ? NSOnState : NSOffState)]; + return S_OK; + } +} + +HRESULT AvnAppMenuItem::SetToggleType(AvnMenuItemToggleType toggleType) +{ + @autoreleasepool + { + switch(toggleType) + { + case AvnMenuItemToggleType::None: + [_native setOnStateImage: [NSImage imageNamed:@"NSMenuCheckmark"]]; + + _isCheckable = false; + break; + + case AvnMenuItemToggleType::CheckMark: + [_native setOnStateImage: [NSImage imageNamed:@"NSMenuCheckmark"]]; + + _isCheckable = true; + break; + + case AvnMenuItemToggleType::Radio: + [_native setOnStateImage: [NSImage imageNamed:@"NSMenuItemBullet"]]; + + _isCheckable = true; + break; + } + + return S_OK; + } +} + +HRESULT AvnAppMenuItem::SetIcon(void *data, size_t length) +{ + @autoreleasepool + { + if(data != nullptr) + { + NSData *imageData = [NSData dataWithBytes:data length:length]; + NSImage *image = [[NSImage alloc] initWithData:imageData]; + + NSSize originalSize = [image size]; + + NSSize size; + size.height = [[NSFont menuFontOfSize:0] pointSize] * 1.333333; + + auto scaleFactor = size.height / originalSize.height; + size.width = originalSize.width * scaleFactor; + + [image setSize: size]; + [_native setImage:image]; + } + else + { + [_native setImage:nullptr]; + } + return S_OK; + } } bool AvnAppMenuItem::EvaluateItemEnabled() @@ -130,71 +240,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 +364,7 @@ extern IAvnAppMenuItem* CreateAppMenuItem() } } -extern IAvnAppMenuItem* CreateAppMenuItemSeperator() +extern IAvnMenuItem* CreateAppMenuItemSeperator() { @autoreleasepool { @@ -210,10 +372,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 +456,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..bea751ad4c 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -14,9 +14,9 @@ - + - + @@ -36,6 +36,24 @@ + + + + + + + + + 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.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index b1d4e0e58f..ca1d97290e 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -251,10 +251,10 @@ namespace Avalonia.Animation if (keyframe.TimingMode == KeyFrameTimingMode.TimeSpan) { - cue = new Cue(keyframe.KeyTime.Ticks / Duration.Ticks); + cue = new Cue(keyframe.KeyTime.TotalSeconds / Duration.TotalSeconds); } - var newKF = new AnimatorKeyFrame(handler, cue); + var newKF = new AnimatorKeyFrame(handler, cue, keyframe.KeySpline); subscriptions.Add(newKF.BindSetter(setter, control)); diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs index 36d15e518e..f6a0c12be4 100644 --- a/src/Avalonia.Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Animation/AnimatorKeyFrame.cs @@ -24,11 +24,20 @@ namespace Avalonia.Animation { AnimatorType = animatorType; Cue = cue; + KeySpline = null; + } + + public AnimatorKeyFrame(Type animatorType, Cue cue, KeySpline keySpline) + { + AnimatorType = animatorType; + Cue = cue; + KeySpline = keySpline; } internal bool isNeutral; public Type AnimatorType { get; } public Cue Cue { get; } + public KeySpline KeySpline { get; } public AvaloniaProperty Property { get; private set; } private object _value; diff --git a/src/Avalonia.Animation/Animators/Animator`1.cs b/src/Avalonia.Animation/Animators/Animator`1.cs index aa5e6aaf14..0660440e30 100644 --- a/src/Avalonia.Animation/Animators/Animator`1.cs +++ b/src/Avalonia.Animation/Animators/Animator`1.cs @@ -89,6 +89,9 @@ namespace Avalonia.Animation.Animators else newValue = (T)lastKeyframe.Value; + if (lastKeyframe.KeySpline != null) + progress = lastKeyframe.KeySpline.GetSplineProgress(progress); + return Interpolate(progress, oldValue, newValue); } diff --git a/src/Avalonia.Animation/KeyFrame.cs b/src/Avalonia.Animation/KeyFrame.cs index ec59586584..c2cc1aa051 100644 --- a/src/Avalonia.Animation/KeyFrame.cs +++ b/src/Avalonia.Animation/KeyFrame.cs @@ -19,6 +19,7 @@ namespace Avalonia.Animation { private TimeSpan _ktimeSpan; private Cue _kCue; + private KeySpline _kKeySpline; public KeyFrame() { @@ -74,6 +75,25 @@ namespace Avalonia.Animation } } + /// + /// Gets or sets the KeySpline of this . + /// + /// The key spline. + public KeySpline KeySpline + { + get + { + return _kKeySpline; + } + set + { + _kKeySpline = value; + if (value != null && !value.IsValid()) + { + throw new ArgumentException($"{nameof(KeySpline)} must have X coordinates >= 0.0 and <= 1.0."); + } + } + } } diff --git a/src/Avalonia.Animation/KeySpline.cs b/src/Avalonia.Animation/KeySpline.cs new file mode 100644 index 0000000000..5a4f7a15a3 --- /dev/null +++ b/src/Avalonia.Animation/KeySpline.cs @@ -0,0 +1,349 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Text; +using Avalonia; +using Avalonia.Utilities; + +// Ported from WPF open-source code. +// https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs + +namespace Avalonia.Animation +{ + /// + /// Determines how an animation is used based on a cubic bezier curve. + /// X1 and X2 must be between 0.0 and 1.0, inclusive. + /// See https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.animation.keyspline + /// + [TypeConverter(typeof(KeySplineTypeConverter))] + public class KeySpline : AvaloniaObject + { + // Control points + private double _controlPointX1; + private double _controlPointY1; + private double _controlPointX2; + private double _controlPointY2; + private bool _isSpecified; + private bool _isDirty; + + // The parameter that corresponds to the most recent time + private double _parameter; + + // Cached coefficients + private double _Bx; // 3*points[0].X + private double _Cx; // 3*points[1].X + private double _Cx_Bx; // 2*(Cx - Bx) + private double _three_Cx; // 3 - Cx + + private double _By; // 3*points[0].Y + private double _Cy; // 3*points[1].Y + + // constants + private const double _accuracy = .001; // 1/3 the desired accuracy in X + private const double _fuzz = .000001; // computational zero + + /// + /// Create a with X1 = Y1 = 0 and X2 = Y2 = 1. + /// + public KeySpline() + { + _controlPointX1 = 0.0; + _controlPointY1 = 0.0; + _controlPointX2 = 1.0; + _controlPointY2 = 1.0; + _isDirty = true; + } + + /// + /// Create a with the given parameters + /// + /// X coordinate for the first control point + /// Y coordinate for the first control point + /// X coordinate for the second control point + /// Y coordinate for the second control point + public KeySpline(double x1, double y1, double x2, double y2) + { + _controlPointX1 = x1; + _controlPointY1 = y1; + _controlPointX2 = x2; + _controlPointY2 = y2; + _isDirty = true; + } + + /// + /// Parse a from a string. The string + /// needs to contain 4 values in it for the 2 control points. + /// + /// string with 4 values in it + /// culture of the string + /// Thrown if the string does not have 4 values + /// A with the appropriate values set + public static KeySpline Parse(string value, CultureInfo culture) + { + using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid KeySpline.")) + { + return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble()); + } + } + + /// + /// X coordinate of the first control point + /// + public double ControlPointX1 + { + get => _controlPointX1; + set + { + if (IsValidXValue(value)) + { + _controlPointX1 = value; + } + else + { + throw new ArgumentException("Invalid KeySpline X1 value. Must be >= 0.0 and <= 1.0."); + } + } + } + + /// + /// Y coordinate of the first control point + /// + public double ControlPointY1 + { + get => _controlPointY1; + set => _controlPointY1 = value; + } + + /// + /// X coordinate of the second control point + /// + public double ControlPointX2 + { + get => _controlPointX2; + set + { + if (IsValidXValue(value)) + { + _controlPointX2 = value; + } + else + { + throw new ArgumentException("Invalid KeySpline X2 value. Must be >= 0.0 and <= 1.0."); + } + } + } + + /// + /// Y coordinate of the second control point + /// + public double ControlPointY2 + { + get => _controlPointY2; + set => _controlPointY2 = value; + } + + /// + /// Calculates spline progress from a linear progress. + /// + /// the linear progress + /// the spline progress + public double GetSplineProgress(double linearProgress) + { + if (_isDirty) + { + Build(); + } + + if (!_isSpecified) + { + return linearProgress; + } + else + { + SetParameterFromX(linearProgress); + + return GetBezierValue(_By, _Cy, _parameter); + } + } + + /// + /// Check to see whether the is valid by looking + /// at its X values. + /// + /// true if the X values for this fall in + /// acceptable range; false otherwise. + public bool IsValid() + { + return IsValidXValue(_controlPointX1) && IsValidXValue(_controlPointX2); + } + + /// + /// + /// + /// + /// + private bool IsValidXValue(double value) + { + return value >= 0.0 && value <= 1.0; + } + + /// + /// Compute cached coefficients. + /// + private void Build() + { + if (_controlPointX1 == 0 && _controlPointY1 == 0 && _controlPointX2 == 1 && _controlPointY2 == 1) + { + // This KeySpline would have no effect on the progress. + _isSpecified = false; + } + else + { + _isSpecified = true; + + _parameter = 0; + + // X coefficients + _Bx = 3 * _controlPointX1; + _Cx = 3 * _controlPointX2; + _Cx_Bx = 2 * (_Cx - _Bx); + _three_Cx = 3 - _Cx; + + // Y coefficients + _By = 3 * _controlPointY1; + _Cy = 3 * _controlPointY2; + } + + _isDirty = false; + } + + /// + /// Get an X or Y value with the Bezier formula. + /// + /// the second Bezier coefficient + /// the third Bezier coefficient + /// the parameter value to evaluate at + /// the value of the Bezier function at the given parameter + static private double GetBezierValue(double b, double c, double t) + { + double s = 1.0 - t; + double t2 = t * t; + + return b * t * s * s + c * t2 * s + t2 * t; + } + + /// + /// Get X and dX/dt at a given parameter + /// + /// the parameter value to evaluate at + /// the value of x there + /// the value of dx/dt there + private void GetXAndDx(double t, out double x, out double dx) + { + double s = 1.0 - t; + double t2 = t * t; + double s2 = s * s; + + x = _Bx * t * s2 + _Cx * t2 * s + t2 * t; + dx = _Bx * s2 + _Cx_Bx * s * t + _three_Cx * t2; + } + + /// + /// Compute the parameter value that corresponds to a given X value, using a modified + /// clamped Newton-Raphson algorithm to solve the equation X(t) - time = 0. We make + /// use of some known properties of this particular function: + /// * We are only interested in solutions in the interval [0,1] + /// * X(t) is increasing, so we can assume that if X(t) > time t > solution. We use + /// that to clamp down the search interval with every probe. + /// * The derivative of X and Y are between 0 and 3. + /// + /// the time, scaled to fit in [0,1] + private void SetParameterFromX(double time) + { + // Dynamic search interval to clamp with + double bottom = 0; + double top = 1; + + if (time == 0) + { + _parameter = 0; + } + else if (time == 1) + { + _parameter = 1; + } + else + { + // Loop while improving the guess + while (top - bottom > _fuzz) + { + double x, dx, absdx; + + // Get x and dx/dt at the current parameter + GetXAndDx(_parameter, out x, out dx); + absdx = Math.Abs(dx); + + // Clamp down the search interval, relying on the monotonicity of X(t) + if (x > time) + { + top = _parameter; // because parameter > solution + } + else + { + bottom = _parameter; // because parameter < solution + } + + // The desired accuracy is in ultimately in y, not in x, so the + // accuracy needs to be multiplied by dx/dy = (dx/dt) / (dy/dt). + // But dy/dt <=3, so we omit that + if (Math.Abs(x - time) < _accuracy * absdx) + { + break; // We're there + } + + if (absdx > _fuzz) + { + // Nonzero derivative, use Newton-Raphson to obtain the next guess + double next = _parameter - (x - time) / dx; + + // If next guess is out of the search interval then clamp it in + if (next >= top) + { + _parameter = (_parameter + top) / 2; + } + else if (next <= bottom) + { + _parameter = (_parameter + bottom) / 2; + } + else + { + // Next guess is inside the search interval, accept it + _parameter = next; + } + } + else // Zero derivative, halve the search interval + { + _parameter = (bottom + top) / 2; + } + } + } + } + } + + /// + /// Converts string values to values + /// + public class KeySplineTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + return KeySpline.Parse((string)value, culture); + } + } +} diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index 09c3d07a41..8e82bf1a38 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -55,7 +55,7 @@ namespace Avalonia.Controls binding.Mode = BindingMode.TwoWay; } - if (binding.Converter == null) + if (binding.Converter == null && string.IsNullOrEmpty(binding.StringFormat)) { binding.Converter = DataGridValueConverter.Instance; } diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs index b014c699bb..0f513e7f42 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs @@ -269,6 +269,9 @@ namespace Avalonia.Controls.Primitives // Since we didn't know the final widths of the columns until we resized, // we waited until now to measure each cell double leftEdge = 0; + if (autoSizeHeight) + DesiredHeight = 0; + foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetVisibleColumns()) { DataGridCell cell = OwningRow.Cells[column.Index]; diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 1daa6a5630..67ef6cd1e9 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -234,6 +234,23 @@ namespace Avalonia.Controls base.OnTemplateApplied(e); } + /// + /// Called when the ComboBox popup is closed, with the + /// that caused the popup to close. + /// + /// The event args. + /// + /// This method can be overridden to control whether the event that caused the popup to close + /// is swallowed or passed through. + /// + protected virtual void PopupClosedOverride(PopupClosedEventArgs e) + { + if (e.CloseEvent is PointerEventArgs pointerEvent) + { + pointerEvent.Handled = true; + } + } + internal void ItemFocused(ComboBoxItem dropDownItem) { if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid) @@ -247,10 +264,7 @@ namespace Avalonia.Controls _subscriptionsOnOpen?.Dispose(); _subscriptionsOnOpen = null; - if (e.CloseEvent is PointerEventArgs pointerEvent) - { - pointerEvent.Handled = true; - } + PopupClosedOverride(e); if (CanFocus(this)) { 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..4c94d82eb4 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -1,15 +1,20 @@ using System; using System.Windows.Input; using Avalonia.Input; +using Avalonia.Media.Imaging; 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 NativeMenuItemToggleType _toggleType; + private IBitmap _icon; private NativeMenu _menu; @@ -55,13 +60,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 { @@ -74,39 +73,63 @@ namespace Avalonia.Controls } } + public static readonly DirectProperty IconProperty = + AvaloniaProperty.RegisterDirect(nameof(Icon), o => o.Icon, (o, v) => o.Icon = v); + + + public IBitmap Icon + { + get => _icon; + set => SetAndRaise(IconProperty, ref _icon, value); + } + 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 ToggleTypeProperty = + AvaloniaProperty.RegisterDirect( + nameof(ToggleType), + o => o.ToggleType, + (o, v) => o.ToggleType = v); + + public NativeMenuItemToggleType ToggleType + { + get => _toggleType; + set => SetAndRaise(ToggleTypeProperty, ref _toggleType, 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 +137,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 +184,7 @@ namespace Avalonia.Controls public event EventHandler Clicked; - public void RaiseClick() + void INativeMenuItemExporterEventsImplBridge.RaiseClicked() { Clicked?.Invoke(this, new EventArgs()); @@ -159,4 +194,11 @@ namespace Avalonia.Controls } } } + + public enum NativeMenuItemToggleType + { + None, + CheckBox, + Radio + } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index dd00b850fe..75f32c862e 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -500,6 +500,19 @@ namespace Avalonia.Controls EnsureInitialized(); IsVisible = true; + + var initialSize = new Size( + double.IsNaN(Width) ? ClientSize.Width : Width, + double.IsNaN(Height) ? ClientSize.Height : Height); + + if (initialSize != ClientSize) + { + using (BeginAutoSizing()) + { + PlatformImpl?.Resize(initialSize); + } + } + LayoutManager.ExecuteInitialLayoutPass(this); var result = new TaskCompletionSource(); diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 90239b5a49..e93ca64d3a 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.IO; using System.Reactive.Disposables; using System.Threading.Tasks; using Avalonia.Controls; @@ -184,7 +185,7 @@ namespace Avalonia.FreeDesktop private static string[] AllProperties = new[] { - "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display" + "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data" }; object GetProperty((NativeMenuItemBase item, NativeMenu menu) i, string name) @@ -210,7 +211,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; } @@ -234,6 +235,30 @@ namespace Avalonia.FreeDesktop return new[] { lst.ToArray() }; } + if (name == "toggle-type") + { + if (item.ToggleType == NativeMenuItemToggleType.CheckBox) + return "checkmark"; + if (item.ToggleType == NativeMenuItemToggleType.Radio) + return "radio"; + } + + if (name == "toggle-state") + { + if (item.ToggleType != NativeMenuItemToggleType.None) + return item.IsChecked ? 1 : 0; + } + + if (name == "icon-data") + { + if (item.Icon != null) + { + var ms = new MemoryStream(); + item.Icon.Save(ms); + return ms.ToArray(); + } + } + if (name == "children-display") return menu != null ? "submenu" : null; } @@ -319,10 +344,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..c8819d1994 --- /dev/null +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -0,0 +1,175 @@ +using System; +using System.IO; +using System.Reactive.Disposables; +using Avalonia.Controls; +using Avalonia.Media.Imaging; +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 UpdateToggleType(NativeMenuItemToggleType toggleType) + { + ToggleType = (AvnMenuItemToggleType)toggleType; + } + + private unsafe void UpdateIcon (IBitmap icon) + { + if(icon is null) + { + SetIcon(IntPtr.Zero, 0); + } + else + { + using(var ms = new MemoryStream()) + { + icon.Save(ms); + + var imageData = ms.ToArray(); + + fixed(void* ptr = imageData) + { + SetIcon(new IntPtr(ptr), imageData.Length); + } + } + } + } + + 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); + + UpdateToggleType(item.ToggleType); + + UpdateIcon(item.Icon); + + 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.ToggleTypeProperty) + .Subscribe(x => UpdateToggleType(x))); + + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.IsCheckedProperty) + .Subscribe(x => UpdateIsChecked(x))); + + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.IconProperty) + .Subscribe(x => UpdateIcon(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(); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs index ebe4035ed6..a1fe6976b7 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -100,6 +100,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions => AddType(typeSystem.GetType(type), typeSystem.GetType(conv)); Add("Avalonia.Media.IImage","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter"); + Add("Avalonia.Media.Imaging.IBitmap","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter"); var ilist = typeSystem.GetType("System.Collections.Generic.IList`1"); AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")), typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter")); diff --git a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs index f7a8774689..fe718ec32b 100644 --- a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs +++ b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs @@ -14,6 +14,55 @@ namespace Avalonia.Animation.UnitTests { public class AnimationIterationTests { + [Fact] + public void Check_KeyTime_Correctly_Converted_To_Cue() + { + var keyframe1 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 100d), + }, + KeyTime = TimeSpan.FromSeconds(0.5) + }; + + var keyframe2 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 0d), + }, + KeyTime = TimeSpan.FromSeconds(0) + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(1), + Children = + { + keyframe2, + keyframe1 + } + }; + + var border = new Border() + { + Height = 100d, + Width = 100d + }; + + var clock = new TestClock(); + var animationRun = animation.RunAsync(border, clock); + + clock.Step(TimeSpan.Zero); + Assert.Equal(border.Width, 0d); + + clock.Step(TimeSpan.FromSeconds(1)); + Assert.Equal(border.Width, 100d); + + } + + [Fact] public void Check_Initial_Inter_and_Trailing_Delay_Values() { diff --git a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs new file mode 100644 index 0000000000..df7c0693e1 --- /dev/null +++ b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs @@ -0,0 +1,145 @@ +using System; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.Styling; +using Xunit; + +namespace Avalonia.Animation.UnitTests +{ + public class KeySplineTests + { + [Theory] + [InlineData("1,2 3,4")] + [InlineData("1 2 3 4")] + [InlineData("1 2,3 4")] + [InlineData("1,2,3,4")] + public void Can_Parse_KeySpline_Via_TypeConverter(string input) + { + var conv = new KeySplineTypeConverter(); + + var keySpline = (KeySpline)conv.ConvertFrom(input); + + Assert.Equal(1, keySpline.ControlPointX1); + Assert.Equal(2, keySpline.ControlPointY1); + Assert.Equal(3, keySpline.ControlPointX2); + Assert.Equal(4, keySpline.ControlPointY2); + } + + [Theory] + [InlineData(0.00)] + [InlineData(0.50)] + [InlineData(1.00)] + public void KeySpline_X_Values_In_Range_Do_Not_Throw(double input) + { + var keySpline = new KeySpline(); + keySpline.ControlPointX1 = input; // no exception will be thrown -- test will fail if exception thrown + keySpline.ControlPointX2 = input; // no exception will be thrown -- test will fail if exception thrown + } + + [Theory] + [InlineData(-0.01)] + [InlineData(1.01)] + public void KeySpline_X_Values_Cannot_Be_Out_Of_Range(double input) + { + var keySpline = new KeySpline(); + Assert.Throws(() => keySpline.ControlPointX1 = input); + Assert.Throws(() => keySpline.ControlPointX2 = input); + } + + /* + To get the test values for the KeySpline test, you can: + 1) Grab the WPF sample for KeySpline animations from https://github.com/microsoft/WPF-Samples/tree/master/Animation/KeySplineAnimations + 2) Add the following xaml somewhere: +