diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 08a9aa3ceb..4f69f39e02 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 3475eff654..38d99db5c9 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, @@ -133,6 +135,7 @@ enum AvnWindowState Normal, Minimized, Maximized, + FullScreen, }; enum AvnStandardCursorType @@ -175,6 +178,13 @@ enum AvnWindowEdge WindowEdgeSouthEast }; +enum AvnMenuItemToggleType +{ + None, + CheckMark, + Radio +}; + AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown { public: @@ -188,11 +198,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 +231,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; @@ -239,7 +247,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase { virtual HRESULT ShowDialog (IAvnWindow* parent) = 0; virtual HRESULT SetCanResize(bool value) = 0; - virtual HRESULT SetHasDecorations(SystemDecorations value) = 0; + virtual HRESULT SetDecorations(SystemDecorations value) = 0; virtual HRESULT SetTitle (void* utf8Title) = 0; virtual HRESULT SetTitleBarColor (AvnColor color) = 0; virtual HRESULT SetWindowState(AvnWindowState state) = 0; @@ -388,10 +396,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 +409,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 { @@ -14,6 +15,10 @@ extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationA } [[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy]; + + [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"NSFullScreenMenuItemEverywhere"]; + + [[NSApplication sharedApplication] setHelpMenu: [[NSMenu new] initWithTitle:@""]]; } } 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..ec8fe9e6ee 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -19,7 +19,11 @@ class WindowBaseImpl; -(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) restoreParentWindow; -(bool) shouldTryToHandleEvents; --(void) applyMenu:(NSMenu *)menu; +-(bool) isModal; +-(void) setModal: (bool) isModal; +-(void) showAppMenuOnly; +-(void) showWindowMenuWithAppMenu; +-(void) applyMenu:(NSMenu* _Nullable)menu; -(double) getScaling; @end @@ -31,6 +35,10 @@ struct INSWindowHolder struct IWindowStateChanged { virtual void WindowStateChanged () = 0; + virtual void StartStateTransition () = 0; + virtual void EndStateTransition () = 0; + virtual SystemDecorations Decorations () = 0; + virtual AvnWindowState WindowState () = 0; }; #endif /* window_h */ diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 6298118c10..091219fc72 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; } @@ -398,7 +391,7 @@ protected: void UpdateStyle() { - [Window setStyleMask:GetStyle()]; + [Window setStyleMask: GetStyle()]; } public: @@ -411,10 +404,13 @@ public: class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged { private: - bool _canResize = true; - SystemDecorations _hasDecorations = SystemDecorationsFull; - CGRect _lastUndecoratedFrame; + bool _canResize; + bool _fullScreenActive; + SystemDecorations _decorations; AvnWindowState _lastWindowState; + bool _inSetWindowState; + NSRect _preZoomSize; + bool _transitioningWindowState; FORWARD_IUNKNOWN() BEGIN_INTERFACE_MAP() @@ -428,10 +424,30 @@ private: ComPtr WindowEvents; WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) { + _fullScreenActive = false; + _canResize = true; + _decorations = SystemDecorationsFull; + _transitioningWindowState = false; + _inSetWindowState = false; _lastWindowState = Normal; WindowEvents = events; [Window setCanBecomeKeyAndMain]; [Window disableCursorRects]; + [Window setTabbingMode:NSWindowTabbingModeDisallowed]; + } + + void HideOrShowTrafficLights () + { + for (id subview in Window.contentView.superview.subviews) { + if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) { + NSView *titlebarView = [subview subviews][0]; + for (id button in titlebarView.subviews) { + if ([button isKindOfClass:[NSButton class]]) { + [button setHidden: (_decorations != SystemDecorationsFull)]; + } + } + } + } } virtual HRESULT Show () override @@ -440,8 +456,13 @@ private: { if([Window parentWindow] != nil) [[Window parentWindow] removeChildWindow:Window]; + + [Window setModal:FALSE]; + WindowBaseImpl::Show(); + HideOrShowTrafficLights(); + return SetWindowState(_lastWindowState); } } @@ -457,44 +478,74 @@ private: if(cparent == nullptr) return E_INVALIDARG; + [Window setModal:TRUE]; + [cparent->Window addChildWindow:Window ordered:NSWindowAbove]; WindowBaseImpl::Show(); + HideOrShowTrafficLights(); + return S_OK; } } + void StartStateTransition () override + { + _transitioningWindowState = true; + } + + void EndStateTransition () override + { + _transitioningWindowState = false; + } + + SystemDecorations Decorations () override + { + return _decorations; + } + + AvnWindowState WindowState () override + { + return _lastWindowState; + } + void WindowStateChanged () override { - AvnWindowState state; - GetWindowState(&state); - WindowEvents->WindowStateChanged(state); + if(!_inSetWindowState && !_transitioningWindowState) + { + AvnWindowState state; + GetWindowState(&state); + + if(_lastWindowState != state) + { + _lastWindowState = state; + WindowEvents->WindowStateChanged(state); + } + } } bool UndecoratedIsMaximized () { - return CGRectEqualToRect([Window frame], [Window screen].visibleFrame); + auto windowSize = [Window frame]; + auto available = [Window screen].visibleFrame; + return CGRectEqualToRect(windowSize, available); } bool IsZoomed () { - return _hasDecorations != SystemDecorationsNone ? [Window isZoomed] : UndecoratedIsMaximized(); + return _decorations == SystemDecorationsFull ? [Window isZoomed] : UndecoratedIsMaximized(); } void DoZoom() { - switch (_hasDecorations) + switch (_decorations) { case SystemDecorationsNone: - if (!UndecoratedIsMaximized()) - { - _lastUndecoratedFrame = [Window frame]; - } - - [Window zoom:Window]; + case SystemDecorationsBorderOnly: + [Window setFrame:[Window screen].visibleFrame display:true]; break; - case SystemDecorationsBorderOnly: + case SystemDecorationsFull: [Window performZoom:Window]; break; @@ -511,25 +562,52 @@ private: } } - virtual HRESULT SetHasDecorations(SystemDecorations value) override + virtual HRESULT SetDecorations(SystemDecorations value) override { @autoreleasepool { - _hasDecorations = value; + auto currentWindowState = _lastWindowState; + _decorations = value; + + if(_fullScreenActive) + { + return S_OK; + } + + auto currentFrame = [Window frame]; + UpdateStyle(); + + HideOrShowTrafficLights(); - switch (_hasDecorations) + switch (_decorations) { case SystemDecorationsNone: [Window setHasShadow:NO]; [Window setTitleVisibility:NSWindowTitleHidden]; [Window setTitlebarAppearsTransparent:YES]; + + if(currentWindowState == Maximized) + { + if(!UndecoratedIsMaximized()) + { + DoZoom(); + } + } break; case SystemDecorationsBorderOnly: [Window setHasShadow:YES]; [Window setTitleVisibility:NSWindowTitleHidden]; [Window setTitlebarAppearsTransparent:YES]; + + if(currentWindowState == Maximized) + { + if(!UndecoratedIsMaximized()) + { + DoZoom(); + } + } break; case SystemDecorationsFull: @@ -537,6 +615,13 @@ private: [Window setTitleVisibility:NSWindowTitleVisible]; [Window setTitlebarAppearsTransparent:NO]; [Window setTitle:_lastTitle]; + + if(currentWindowState == Maximized) + { + auto newFrame = [Window contentRectForFrameRect:[Window frame]].size; + + [View setFrameSize:newFrame]; + } break; } @@ -593,13 +678,19 @@ private: return E_POINTER; } + if(([Window styleMask] & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen) + { + *ret = FullScreen; + return S_OK; + } + if([Window isMiniaturized]) { *ret = Minimized; return S_OK; } - if([Window isZoomed]) + if(IsZoomed()) { *ret = Maximized; return S_OK; @@ -611,16 +702,57 @@ private: } } + void EnterFullScreenMode () + { + _fullScreenActive = true; + + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleVisible]; + [Window setTitlebarAppearsTransparent:NO]; + [Window setTitle:_lastTitle]; + + [Window setStyleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskResizable]; + + [Window toggleFullScreen:nullptr]; + } + + void ExitFullScreenMode () + { + [Window toggleFullScreen:nullptr]; + + _fullScreenActive = false; + + SetDecorations(_decorations); + } + virtual HRESULT SetWindowState (AvnWindowState state) override { @autoreleasepool { + if(_lastWindowState == state) + { + return S_OK; + } + + _inSetWindowState = true; + + auto currentState = _lastWindowState; _lastWindowState = state; + if(currentState == Normal) + { + _preZoomSize = [Window frame]; + } + if(_shown) { switch (state) { case Maximized: + if(currentState == FullScreen) + { + ExitFullScreenMode(); + } + lastPositionSet.X = 0; lastPositionSet.Y = 0; @@ -636,40 +768,66 @@ private: break; case Minimized: - [Window miniaturize:Window]; + if(currentState == FullScreen) + { + ExitFullScreenMode(); + } + else + { + [Window miniaturize:Window]; + } break; - default: + case FullScreen: if([Window isMiniaturized]) { [Window deminiaturize:Window]; } + EnterFullScreenMode(); + break; + + case Normal: + if([Window isMiniaturized]) + { + [Window deminiaturize:Window]; + } + + if(currentState == FullScreen) + { + ExitFullScreenMode(); + } + if(IsZoomed()) { - DoZoom(); + if(_decorations == SystemDecorationsFull) + { + DoZoom(); + } + else + { + [Window setFrame:_preZoomSize display:true]; + auto newFrame = [Window contentRectForFrameRect:[Window frame]].size; + + [View setFrameSize:newFrame]; + } + } break; } } + _inSetWindowState = false; + return S_OK; } } virtual void OnResized () override { - if(_shown) + if(_shown && !_inSetWindowState && !_transitioningWindowState) { - auto windowState = [Window isMiniaturized] ? Minimized - : (IsZoomed() ? Maximized : Normal); - - if (windowState != _lastWindowState) - { - _lastWindowState = windowState; - - WindowEvents->WindowStateChanged(windowState); - } + WindowStateChanged(); } } @@ -678,22 +836,23 @@ protected: { unsigned long s = NSWindowStyleMaskBorderless; - switch (_hasDecorations) + switch (_decorations) { case SystemDecorationsNone: + s = s | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable; break; case SystemDecorationsBorderOnly: - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable; break; case SystemDecorationsFull: s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless; + if(_canResize) { s = s | NSWindowStyleMaskResizable; } - break; } @@ -1151,8 +1310,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent ComPtr _parent; bool _canBecomeKeyAndMain; bool _closed; - NSMenu* _menu; - bool _isAppMenuApplied; + bool _isModal; + AvnMenu* _menu; double _lastScaling; } @@ -1172,6 +1331,20 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } +- (void)performClose:(id)sender +{ + if([[self delegate] respondsToSelector:@selector(windowShouldClose:)]) + { + if(![[self delegate] windowShouldClose:self]) return; + } + else if([self respondsToSelector:@selector(windowShouldClose:)]) + { + if(![self windowShouldClose:self]) return; + } + + [self close]; +} + - (void)pollModalSession:(nonnull NSModalSession)session { auto response = [NSApp runModalSession:session]; @@ -1189,32 +1362,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(); + + auto nativeAppMenu = dynamic_cast(appMenu); - if(appMenu != nullptr) + [[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; @@ -1298,11 +1503,25 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent auto ch = objc_cast(uch); if(ch == nil) continue; + + if(![ch isModal]) + continue; + return FALSE; } return TRUE; } +-(bool) isModal +{ + return _isModal; +} + +-(void) setModal: (bool) isModal +{ + _isModal = isModal; +} + -(void)makeKeyWindow { if([self activateAppropriateChild: true]) @@ -1315,23 +1534,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]; @@ -1370,39 +1573,79 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)windowDidResize:(NSNotification *)notification { - _parent->OnResized(); + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->WindowStateChanged(); + } } -- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame +- (void)windowWillExitFullScreen:(NSNotification *)notification { - return true; + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->StartStateTransition(); + } } --(void)resignKeyWindow +- (void)windowDidExitFullScreen:(NSNotification *)notification { - if(_parent) - _parent->BaseEvents->Deactivated(); - - auto appMenuItem = ::GetAppMenuItem(); + auto parent = dynamic_cast(_parent.operator->()); - if(appMenuItem != nullptr) + if(parent != nullptr) { - auto appMenu = ::GetAppMenu(); + parent->EndStateTransition(); - auto nativeAppMenu = dynamic_cast(appMenu); - - [[appMenuItem menu] removeItem:appMenuItem]; + if(parent->Decorations() != SystemDecorationsFull && parent->WindowState() == Maximized) + { + NSRect screenRect = [[self screen] visibleFrame]; + [self setFrame:screenRect display:YES]; + } - [nativeAppMenu->GetNative() addItem:appMenuItem]; + if(parent->WindowState() == Minimized) + { + [self miniaturize:nullptr]; + } - [NSApp setMenu:nativeAppMenu->GetNative()]; + parent->WindowStateChanged(); } - else +} + +- (void)windowWillEnterFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) { - [NSApp setMenu:nullptr]; + parent->StartStateTransition(); + } +} + +- (void)windowDidEnterFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->EndStateTransition(); + parent->WindowStateChanged(); } +} + +- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame +{ + return true; +} + +-(void)resignKeyWindow +{ + if(_parent) + _parent->BaseEvents->Deactivated(); - // remove window menu items from appmenu? + [self showAppMenuOnly]; [super resignKeyWindow]; } diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index f3f70719e3..e02308b5c6 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -59,8 +59,8 @@ - - + + No Decorations Border Only Full Decorations @@ -69,6 +69,7 @@ Light Dark + diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index d25de9c1f5..935db20757 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -7,16 +7,16 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:v="clr-namespace:ControlCatalog.Views" - x:Class="ControlCatalog.MainWindow"> + x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}"> - + - + @@ -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..0257b4ce66 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -1,4 +1,5 @@ using System.Reactive; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Notifications; using Avalonia.Dialogs; @@ -10,6 +11,10 @@ namespace ControlCatalog.ViewModels { private IManagedNotificationManager _notificationManager; + private bool _isMenuItemChecked = true; + private WindowState _windowState; + private WindowState[] _windowStates; + public MainWindowViewModel(IManagedNotificationManager notificationManager) { _notificationManager = notificationManager; @@ -42,6 +47,33 @@ namespace ControlCatalog.ViewModels { (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown(); }); + + ToggleMenuItemCheckedCommand = ReactiveCommand.Create(() => + { + IsMenuItemChecked = !IsMenuItemChecked; + }); + + WindowState = WindowState.Normal; + + WindowStates = new WindowState[] + { + WindowState.Minimized, + WindowState.Normal, + WindowState.Maximized, + WindowState.FullScreen, + }; + } + + public WindowState WindowState + { + get { return _windowState; } + set { this.RaiseAndSetIfChanged(ref _windowState, value); } + } + + public WindowState[] WindowStates + { + get { return _windowStates; } + set { this.RaiseAndSetIfChanged(ref _windowStates, value); } } public IManagedNotificationManager NotificationManager @@ -50,6 +82,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 +97,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 5dfa7afb41..50f2067df6 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/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 3715bc52a4..8ad622ba4a 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -392,7 +392,7 @@ namespace Avalonia.Controls.Platform { var control = e.Source as ILogical; - if (!Menu.IsLogicalParentOf(control)) + if (!Menu.IsLogicalAncestorOf(control)) { Menu.Close(); } diff --git a/src/Avalonia.Controls/Platform/ISystemDialogImpl.cs b/src/Avalonia.Controls/Platform/ISystemDialogImpl.cs index 6141b6eb19..affec6301b 100644 --- a/src/Avalonia.Controls/Platform/ISystemDialogImpl.cs +++ b/src/Avalonia.Controls/Platform/ISystemDialogImpl.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; -using Avalonia.Platform; namespace Avalonia.Controls.Platform { @@ -14,8 +13,8 @@ namespace Avalonia.Controls.Platform /// The details of the file dialog to show. /// The parent window. /// A task returning the selected filenames. - Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent); + Task ShowFileDialogAsync(FileDialog dialog, Window parent); - Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent); + Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent); } } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 905a14cfee..9cbde72f7f 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -74,16 +74,15 @@ namespace Avalonia.Controls.Presenters static TextPresenter() { - AffectsRender(PasswordCharProperty, - SelectionBrushProperty, SelectionForegroundBrushProperty, - SelectionStartProperty, SelectionEndProperty); + AffectsRender(SelectionBrushProperty); - Observable.Merge( - TextProperty.Changed, - SelectionStartProperty.Changed, - SelectionEndProperty.Changed, - PasswordCharProperty.Changed - ).AddClassHandler((x,_) => x.InvalidateFormattedText()); + Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed, + TextAlignmentProperty.Changed, TextWrappingProperty.Changed, + TextBlock.FontSizeProperty.Changed, TextBlock.FontStyleProperty.Changed, + TextBlock.FontWeightProperty.Changed, TextBlock.FontFamilyProperty.Changed, + SelectionStartProperty.Changed, SelectionEndProperty.Changed, + SelectionForegroundBrushProperty.Changed, PasswordCharProperty.Changed + ).AddClassHandler((x, _) => x.InvalidateFormattedText()); CaretIndexProperty.Changed.AddClassHandler((x, e) => x.CaretIndexChanged((int)e.NewValue)); } @@ -184,7 +183,7 @@ namespace Avalonia.Controls.Presenters { get { - return _formattedText ?? (_formattedText = CreateFormattedText(Bounds.Size, Text)); + return _formattedText ?? (_formattedText = CreateFormattedText()); } } @@ -219,7 +218,7 @@ namespace Avalonia.Controls.Presenters get => GetValue(SelectionForegroundBrushProperty); set => SetValue(SelectionForegroundBrushProperty, value); } - + public IBrush CaretBrush { get => GetValue(CaretBrushProperty); @@ -284,13 +283,9 @@ namespace Avalonia.Controls.Presenters /// protected void InvalidateFormattedText() { - if (_formattedText != null) - { - _constraint = _formattedText.Constraint; - _formattedText = null; - } + _formattedText = null; - InvalidateVisual(); + InvalidateMeasure(); } /// @@ -307,6 +302,7 @@ namespace Avalonia.Controls.Presenters } FormattedText.Constraint = Bounds.Size; + context.DrawText(Foreground, new Point(), FormattedText); } @@ -424,20 +420,20 @@ namespace Avalonia.Controls.Presenters /// /// Creates the used to render the text. /// - /// The constraint of the text. - /// The text to generated the for. /// A object. - protected virtual FormattedText CreateFormattedText(Size constraint, string text) + protected virtual FormattedText CreateFormattedText() { FormattedText result = null; + var text = Text; + if (PasswordChar != default(char)) { - result = CreateFormattedTextInternal(constraint, new string(PasswordChar, text?.Length ?? 0)); + result = CreateFormattedTextInternal(_constraint, new string(PasswordChar, text?.Length ?? 0)); } else { - result = CreateFormattedTextInternal(constraint, text); + result = CreateFormattedTextInternal(_constraint, text); } var selectionStart = SelectionStart; @@ -467,13 +463,15 @@ namespace Avalonia.Controls.Presenters { if (TextWrapping == TextWrapping.Wrap) { - FormattedText.Constraint = new Size(availableSize.Width, double.PositiveInfinity); + _constraint = new Size(availableSize.Width, double.PositiveInfinity); } else { - FormattedText.Constraint = Size.Infinity; + _constraint = Size.Infinity; } + _formattedText = null; + return FormattedText.Bounds.Size; } diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs index 6ccaa3c742..e74b950f23 100644 --- a/src/Avalonia.Controls/SystemDialog.cs +++ b/src/Avalonia.Controls/SystemDialog.cs @@ -32,7 +32,7 @@ namespace Avalonia.Controls if(parent == null) throw new ArgumentNullException(nameof(parent)); return ((await AvaloniaLocator.Current.GetService() - .ShowFileDialogAsync(this, parent?.PlatformImpl)) ?? + .ShowFileDialogAsync(this, parent)) ?? Array.Empty()).FirstOrDefault(); } } @@ -45,7 +45,7 @@ namespace Avalonia.Controls { if(parent == null) throw new ArgumentNullException(nameof(parent)); - return AvaloniaLocator.Current.GetService().ShowFileDialogAsync(this, parent?.PlatformImpl); + return AvaloniaLocator.Current.GetService().ShowFileDialogAsync(this, parent); } } @@ -61,7 +61,7 @@ namespace Avalonia.Controls { if(parent == null) throw new ArgumentNullException(nameof(parent)); - return AvaloniaLocator.Current.GetService().ShowFolderDialogAsync(this, parent?.PlatformImpl); + return AvaloniaLocator.Current.GetService().ShowFolderDialogAsync(this, parent); } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index ee596432f7..75f32c862e 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -484,22 +484,12 @@ namespace Avalonia.Controls /// . /// A task that can be used to retrieve the result of the dialog when it closes. /// - public Task ShowDialog(Window owner) => ShowDialog(owner.PlatformImpl); - - /// - /// Shows the window as a dialog. - /// - /// - /// The type of the result produced by the dialog. - /// - /// The dialog's owner window. - /// . - /// A task that can be used to retrieve the result of the dialog when it closes. - /// - public Task ShowDialog(IWindowImpl owner) + public Task ShowDialog(Window owner) { if (owner == null) + { throw new ArgumentNullException(nameof(owner)); + } if (IsVisible) { @@ -510,29 +500,44 @@ 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(); using (BeginAutoSizing()) { - - PlatformImpl?.ShowDialog(owner); + PlatformImpl?.ShowDialog(owner.PlatformImpl); Renderer?.Start(); + Observable.FromEventPattern( - x => this.Closed += x, - x => this.Closed -= x) + x => Closed += x, + x => Closed -= x) .Take(1) .Subscribe(_ => { owner.Activate(); result.SetResult((TResult)(_dialogResult ?? default(TResult))); }); + OnOpened(EventArgs.Empty); } - SetWindowStartupLocation(owner); + SetWindowStartupLocation(owner.PlatformImpl); + return result.Task; } diff --git a/src/Avalonia.Controls/WindowState.cs b/src/Avalonia.Controls/WindowState.cs index 4ed30e726e..777b52dc11 100644 --- a/src/Avalonia.Controls/WindowState.cs +++ b/src/Avalonia.Controls/WindowState.cs @@ -19,5 +19,10 @@ namespace Avalonia.Controls /// The window is maximized. /// Maximized, + + /// + /// The window is fullscreen. + /// + FullScreen, } } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 7bf1d236bd..82950ce53b 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -166,10 +166,10 @@ namespace Avalonia.DesignerSupport.Remote class SystemDialogsStub : ISystemDialogImpl { - public Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) => + public Task ShowFileDialogAsync(FileDialog dialog, Window parent) => Task.FromResult((string[])null); - public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) => + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) => Task.FromResult((string)null); } diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs b/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs index bf29381eab..7f29407ed5 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs @@ -34,7 +34,7 @@ namespace Avalonia.Dialogs return; } - var isQuickLink = _quickLinksRoot.IsLogicalParentOf(e.Source as Control); + var isQuickLink = _quickLinksRoot.IsLogicalAncestorOf(e.Source as Control); if (e.ClickCount == 2 || isQuickLink) { if (model.ItemType == ManagedFileChooserItemType.File) diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs index 771d2b1b5e..41f526a513 100644 --- a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -1,19 +1,15 @@ -using System; using System.Linq; using System.Threading.Tasks; -using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Platform; -using Avalonia.Dialogs; -using Avalonia.Platform; namespace Avalonia.Dialogs { public static class ManagedFileDialogExtensions { - class ManagedSystemDialogImpl : ISystemDialogImpl where T : Window, new() + private class ManagedSystemDialogImpl : ISystemDialogImpl where T : Window, new() { - async Task Show(SystemDialog d, IWindowImpl parent) + async Task Show(SystemDialog d, Window parent) { var model = new ManagedFileChooserViewModel((FileSystemDialog)d); @@ -39,12 +35,12 @@ namespace Avalonia.Dialogs return result; } - public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + public async Task ShowFileDialogAsync(FileDialog dialog, Window parent) { return await Show(dialog, parent); } - public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) { return (await Show(dialog, parent))?.FirstOrDefault(); } 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/Avalonia.Native/SystemDialogs.cs b/src/Avalonia.Native/SystemDialogs.cs index de355fdf71..d8c2c34f16 100644 --- a/src/Avalonia.Native/SystemDialogs.cs +++ b/src/Avalonia.Native/SystemDialogs.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Native.Interop; -using Avalonia.Platform; namespace Avalonia.Native { @@ -18,13 +17,15 @@ namespace Avalonia.Native _native = native; } - public Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + public Task ShowFileDialogAsync(FileDialog dialog, Window parent) { var events = new SystemDialogEvents(); + var nativeParent = GetNativeWindow(parent); + if (dialog is OpenFileDialog ofd) { - _native.OpenFileDialog((parent as WindowImpl)?.Native, + _native.OpenFileDialog(nativeParent, events, ofd.AllowMultiple, ofd.Title ?? "", ofd.InitialDirectory ?? "", @@ -33,7 +34,7 @@ namespace Avalonia.Native } else { - _native.SaveFileDialog((parent as WindowImpl)?.Native, + _native.SaveFileDialog(nativeParent, events, dialog.Title ?? "", dialog.InitialDirectory ?? "", @@ -44,14 +45,21 @@ namespace Avalonia.Native return events.Task.ContinueWith(t => { events.Dispose(); return t.Result; }); } - public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) { var events = new SystemDialogEvents(); - _native.SelectFolderDialog((parent as WindowImpl)?.Native, events, dialog.Title ?? "", dialog.InitialDirectory ?? ""); + var nativeParent = GetNativeWindow(parent); + + _native.SelectFolderDialog(nativeParent, events, dialog.Title ?? "", dialog.InitialDirectory ?? ""); return events.Task.ContinueWith(t => { events.Dispose(); return t.Result.FirstOrDefault(); }); } + + private IAvnWindow GetNativeWindow(Window window) + { + return (window?.PlatformImpl as WindowImpl)?.Native; + } } public class SystemDialogEvents : CallbackBase, IAvnSystemDialogEvents diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 6d6b2c5296..ec010815f4 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -67,7 +67,7 @@ namespace Avalonia.Native public void SetSystemDecorations(Controls.SystemDecorations enabled) { - _native.HasDecorations = (Interop.SystemDecorations)enabled; + _native.Decorations = (Interop.SystemDecorations)enabled; } public void SetTitleBarColor (Avalonia.Media.Color color) diff --git a/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs b/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs index 1003375978..458ab0fce2 100644 --- a/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs +++ b/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs @@ -1,11 +1,18 @@ using System; using System.Collections.Generic; -using System.Linq; namespace Avalonia.LogicalTree { + /// + /// Provides extension methods for working with the logical tree. + /// public static class LogicalExtensions { + /// + /// Enumerates the ancestors of an in the logical tree. + /// + /// The logical. + /// The logical's ancestors. public static IEnumerable GetLogicalAncestors(this ILogical logical) { Contract.Requires(logical != null); @@ -19,6 +26,11 @@ namespace Avalonia.LogicalTree } } + /// + /// Enumerates an and its ancestors in the logical tree. + /// + /// The logical. + /// The logical and its ancestors. public static IEnumerable GetSelfAndLogicalAncestors(this ILogical logical) { yield return logical; @@ -29,11 +41,50 @@ namespace Avalonia.LogicalTree } } + /// + /// Finds first ancestor of given type. + /// + /// Ancestor type. + /// The logical. + /// If given logical should be included in search. + /// First ancestor of given type. + public static T FindLogicalAncestorOfType(this ILogical logical, bool includeSelf = false) where T : class + { + if (logical is null) + { + return null; + } + + ILogical parent = includeSelf ? logical : logical.LogicalParent; + + while (parent != null) + { + if (parent is T result) + { + return result; + } + + parent = parent.LogicalParent; + } + + return null; + } + + /// + /// Enumerates the children of an in the logical tree. + /// + /// The logical. + /// The logical children. public static IEnumerable GetLogicalChildren(this ILogical logical) { return logical.LogicalChildren; } + /// + /// Enumerates the descendants of an in the logical tree. + /// + /// The logical. + /// The logical's ancestors. public static IEnumerable GetLogicalDescendants(this ILogical logical) { foreach (ILogical child in logical.LogicalChildren) @@ -47,6 +98,11 @@ namespace Avalonia.LogicalTree } } + /// + /// Enumerates an and its descendants in the logical tree. + /// + /// The logical. + /// The logical and its ancestors. public static IEnumerable GetSelfAndLogicalDescendants(this ILogical logical) { yield return logical; @@ -57,16 +113,56 @@ namespace Avalonia.LogicalTree } } + /// + /// Finds first descendant of given type. + /// + /// Descendant type. + /// The logical. + /// If given logical should be included in search. + /// First descendant of given type. + public static T FindLogicalDescendantOfType(this ILogical logical, bool includeSelf = false) where T : class + { + if (logical is null) + { + return null; + } + + if (includeSelf && logical is T result) + { + return result; + } + + return FindDescendantOfTypeCore(logical); + } + + /// + /// Gets the logical parent of an . + /// + /// The logical. + /// The parent, or null if the logical is unparented. public static ILogical GetLogicalParent(this ILogical logical) { return logical.LogicalParent; } + /// + /// Gets the logical parent of an . + /// + /// The type of the logical parent. + /// The logical. + /// + /// The parent, or null if the logical is unparented or its parent is not of type . + /// public static T GetLogicalParent(this ILogical logical) where T : class { return logical.LogicalParent as T; } + /// + /// Enumerates the siblings of an in the logical tree. + /// + /// The logical. + /// The logical siblings. public static IEnumerable GetLogicalSiblings(this ILogical logical) { ILogical parent = logical.LogicalParent; @@ -80,9 +176,55 @@ namespace Avalonia.LogicalTree } } - public static bool IsLogicalParentOf(this ILogical logical, ILogical target) + /// + /// Tests whether an is an ancestor of another logical. + /// + /// The logical. + /// The potential descendant. + /// + /// True if is an ancestor of ; + /// otherwise false. + /// + public static bool IsLogicalAncestorOf(this ILogical logical, ILogical target) { - return target.GetLogicalAncestors().Any(x => x == logical); + ILogical current = target?.LogicalParent; + + while (current != null) + { + if (current == logical) + { + return true; + } + + current = current.LogicalParent; + } + + return false; + } + + private static T FindDescendantOfTypeCore(ILogical logical) where T : class + { + var logicalChildren = logical.LogicalChildren; + var logicalChildrenCount = logicalChildren.Count; + + for (var i = 0; i < logicalChildrenCount; i++) + { + ILogical child = logicalChildren[i]; + + if (child is T result) + { + return result; + } + + var childResult = FindDescendantOfTypeCore(child); + + if (!(childResult is null)) + { + return childResult; + } + } + + return null; } } } diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index 2de629432c..f9410afe6a 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -23,6 +23,11 @@ namespace Avalonia.Media DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName(); + if (string.IsNullOrEmpty(DefaultFontFamilyName)) + { + throw new InvalidOperationException("Default font family name can't be null or empty."); + } + _defaultFontFamily = new FontFamily(DefaultFontFamilyName); } @@ -39,7 +44,8 @@ namespace Avalonia.Media var fontManagerImpl = AvaloniaLocator.Current.GetService(); - if (fontManagerImpl == null) throw new InvalidOperationException("No font manager implementation was registered."); + if (fontManagerImpl == null) + throw new InvalidOperationException("No font manager implementation was registered."); current = new FontManager(fontManagerImpl); @@ -87,7 +93,7 @@ namespace Avalonia.Media fontFamily = _defaultFontFamily; } - var key = new FontKey(fontFamily, fontWeight, fontStyle); + var key = new FontKey(fontFamily.Name, fontWeight, fontStyle); if (_typefaceCache.TryGetValue(key, out var typeface)) { @@ -126,9 +132,21 @@ namespace Avalonia.Media FontStyle fontStyle = FontStyle.Normal, FontFamily fontFamily = null, CultureInfo culture = null) { - return PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ? - _typefaceCache.GetOrAdd(key, new Typeface(key.FontFamily, key.Weight, key.Style)) : + foreach (var cachedTypeface in _typefaceCache.Values) + { + // First try to find a cached typeface by style and weight to avoid redundant glyph index lookup. + if (cachedTypeface.Style == fontStyle && cachedTypeface.Weight == fontWeight + && cachedTypeface.GlyphTypeface.GetGlyph((uint)codepoint) != 0) + { + return cachedTypeface; + } + } + + var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ? + _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Weight, key.Style)) : null; + + return matchedTypeface; } } } diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs index a9ea322d76..96312a5466 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using System.Text; using Avalonia.Utilities; @@ -21,7 +20,7 @@ namespace Avalonia.Media.Fonts throw new ArgumentNullException(nameof(familyNames)); } - Names = familyNames.Split(',').Select(x => x.Trim()).ToArray(); + Names = Array.ConvertAll(familyNames.Split(','), p => p.Trim()); PrimaryFamilyName = Names[0]; diff --git a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs index 1f1e9b067d..a8d81648ba 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs @@ -4,20 +4,20 @@ namespace Avalonia.Media.Fonts { public readonly struct FontKey : IEquatable { - public readonly FontFamily FontFamily; - public readonly FontStyle Style; - public readonly FontWeight Weight; - - public FontKey(FontFamily fontFamily, FontWeight weight, FontStyle style) + public FontKey(string familyName, FontWeight weight, FontStyle style) { - FontFamily = fontFamily; + FamilyName = familyName; Style = style; Weight = weight; } + public string FamilyName { get; } + public FontStyle Style { get; } + public FontWeight Weight { get; } + public override int GetHashCode() { - var hash = FontFamily.GetHashCode(); + var hash = FamilyName.GetHashCode(); hash = hash * 31 + (int)Style; hash = hash * 31 + (int)Weight; @@ -32,7 +32,7 @@ namespace Avalonia.Media.Fonts public bool Equals(FontKey other) { - return FontFamily == other.FontFamily && + return FamilyName == other.FamilyName && Style == other.Style && Weight == other.Weight; } diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs index 69806b22f2..53231ee1dd 100644 --- a/src/Avalonia.Visuals/Media/FormattedText.cs +++ b/src/Avalonia.Visuals/Media/FormattedText.cs @@ -200,7 +200,13 @@ namespace Avalonia.Media private void Set(ref T field, T value) { + if (field != null && field.Equals(value)) + { + return; + } + field = value; + _platformImpl = null; } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs index 7956c5f260..7da39dc5dc 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs @@ -66,7 +66,7 @@ namespace Avalonia.Media.TextFormatting //ToDo: Fix FontFamily fallback currentTypeface = - FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style); + FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style, defaultStyle.TextFormat.Typeface.FontFamily); if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs index 9c20efd867..2ff4952cab 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs @@ -2,7 +2,7 @@ namespace Avalonia.Media.TextFormatting.Unicode { - internal ref struct CodepointEnumerator + public ref struct CodepointEnumerator { private ReadOnlySlice _text; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs deleted file mode 100644 index 3385116f26..0000000000 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace Avalonia.Media.TextFormatting.Unicode -{ - public enum UnicodeGeneralCategory : byte - { - Other, //C# Cc | Cf | Cn | Co | Cs - Control, //Cc - Format, //Cf - Unassigned, //Cn - PrivateUse, //Co - Surrogate, //Cs - Letter, //L# Ll | Lm | Lo | Lt | Lu - CasedLetter, //LC# Ll | Lt | Lu - LowercaseLetter, //Ll - ModifierLetter, //Lm - OtherLetter, //Lo - TitlecaseLetter, //Lt - UppercaseLetter, //Lu - Mark, //M - SpacingMark, //Mc - EnclosingMark, //Me - NonspacingMark, //Mn - Number, //N# Nd | Nl | No - DecimalNumber, //Nd - LetterNumber, //Nl - OtherNumber, //No - Punctuation, //P - ConnectorPunctuation, //Pc - DashPunctuation, //Pd - ClosePunctuation, //Pe - FinalPunctuation, //Pf - InitialPunctuation, //Pi - OtherPunctuation, //Po - OpenPunctuation, //Ps - Symbol, //S# Sc | Sk | Sm | So - CurrencySymbol, //Sc - ModifierSymbol, //Sk - MathSymbol, //Sm - OtherSymbol, //So - Separator, //Z# Zl | Zp | Zs - LineSeparator, //Zl - ParagraphSeparator, //Zp - SpaceSeparator, //Zs - } -} diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index d5e361ca0e..0e6dda1710 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -35,6 +35,7 @@ namespace Avalonia.Rendering private IRef _currentDraw; private readonly IDeferredRendererLock _lock; private readonly object _sceneLock = new object(); + private readonly Action _updateSceneIfNeededDelegate; /// /// Initializes a new instance of the class. @@ -49,7 +50,7 @@ namespace Avalonia.Rendering IRenderLoop renderLoop, ISceneBuilder sceneBuilder = null, IDispatcher dispatcher = null, - IDeferredRendererLock rendererLock = null) + IDeferredRendererLock rendererLock = null) : base(true) { Contract.Requires(root != null); @@ -59,6 +60,7 @@ namespace Avalonia.Rendering Layers = new RenderLayers(); _renderLoop = renderLoop; _lock = rendererLock ?? new ManagedDeferredRendererLock(); + _updateSceneIfNeededDelegate = UpdateSceneIfNeeded; } /// @@ -73,7 +75,7 @@ namespace Avalonia.Rendering public DeferredRenderer( IVisual root, IRenderTarget renderTarget, - ISceneBuilder sceneBuilder = null) + ISceneBuilder sceneBuilder = null) : base(true) { Contract.Requires(root != null); Contract.Requires(renderTarget != null); @@ -83,6 +85,7 @@ namespace Avalonia.Rendering _sceneBuilder = sceneBuilder ?? new SceneBuilder(); Layers = new RenderLayers(); _lock = new ManagedDeferredRendererLock(); + _updateSceneIfNeededDelegate = UpdateSceneIfNeeded; } /// @@ -261,7 +264,8 @@ namespace Avalonia.Rendering try { var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context); - + if (updated) + FpsTick(); using (scene) { if (scene?.Item != null) @@ -318,17 +322,25 @@ namespace Avalonia.Rendering _lastSceneId = scene.Generation; + var isUiThread = Dispatcher.UIThread.CheckAccess(); // We have consumed the previously available scene, but there might be some dirty // rects since the last update. *If* we are on UI thread, we can force immediate scene // rebuild before rendering anything on-screen // We are calling the same method recursively here - if (!recursiveCall && Dispatcher.UIThread.CheckAccess() && NeedsUpdate) + if (!recursiveCall && isUiThread && NeedsUpdate) { UpdateScene(); var (rs, _) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context, true); return (rs, true); } + // We are rendering a new scene version, so it's highly likely + // that there is already a pending update for animations + // So we are scheduling an update call so UI thread could prepare a scene before + // the next render timer tick + if (!recursiveCall && !isUiThread) + Dispatcher.UIThread.Post(_updateSceneIfNeededDelegate, DispatcherPriority.Render); + // Indicate that we have updated the layers return (sceneRef.Clone(), true); } @@ -534,6 +546,12 @@ namespace Avalonia.Rendering context = RenderTarget.CreateDrawingContext(this); } + private void UpdateSceneIfNeeded() + { + if(NeedsUpdate) + UpdateScene(); + } + private void UpdateScene() { Dispatcher.UIThread.VerifyAccess(); diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs index 1e7b5c2923..b37d5d660b 100644 --- a/src/Avalonia.Visuals/Rendering/RendererBase.cs +++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs @@ -7,6 +7,7 @@ namespace Avalonia.Rendering { public class RendererBase { + private readonly bool _useManualFpsCounting; private static int s_fontSize = 18; private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private int _framesThisSecond; @@ -14,8 +15,9 @@ namespace Avalonia.Rendering private FormattedText _fpsText; private TimeSpan _lastFpsUpdate; - public RendererBase() + public RendererBase(bool useManualFpsCounting = false) { + _useManualFpsCounting = useManualFpsCounting; _fpsText = new FormattedText { Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily.Default), @@ -23,12 +25,15 @@ namespace Avalonia.Rendering }; } + protected void FpsTick() => _framesThisSecond++; + protected void RenderFps(IDrawingContextImpl context, Rect clientRect, int? layerCount) { var now = _stopwatch.Elapsed; var elapsed = now - _lastFpsUpdate; - ++_framesThisSecond; + if (!_useManualFpsCounting) + ++_framesThisSecond; if (elapsed.TotalSeconds > 1) { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs index b2e827fa26..0f6001516d 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -162,17 +162,18 @@ namespace Avalonia.Rendering.SceneGraph index.Add(result.Visual, result); - int childCount = source.Children.Count; + var children = source.Children; + var childrenCount = children.Count; - if (childCount > 0) + if (childrenCount > 0) { - Span children = result.AddChildrenSpan(childCount); + result.TryPreallocateChildren(childrenCount); - for (var i = 0; i < childCount; i++) + for (var i = 0; i < childrenCount; i++) { - var child = source.Children[i]; + var child = children[i]; - children[i] = Clone((VisualNode)child, result, index); + result.AddChild(Clone((VisualNode)child, result, index)); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 8cd1a47795..6f566ff6d6 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reactive.Disposables; -using Avalonia.Collections.Pooled; +using Avalonia.Collections; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Utilities; @@ -20,8 +19,8 @@ namespace Avalonia.Rendering.SceneGraph private Rect? _bounds; private double _opacity; - private PooledList _children; - private PooledList> _drawOperations; + private List _children; + private List> _drawOperations; private IRef _drawOperationsRefCounter; private bool _drawOperationsCloned; private Matrix transformRestore; @@ -350,16 +349,9 @@ namespace Avalonia.Rendering.SceneGraph context.Transform = transformRestore; } - /// - /// Inserts default constructed children into collection and returns a span for the newly created range. - /// - /// Count of children that will be added. - /// - internal Span AddChildrenSpan(int count) + internal void TryPreallocateChildren(int count) { EnsureChildrenCreated(count); - - return _children.AddSpan(count); } private Rect CalculateBounds() @@ -379,7 +371,7 @@ namespace Avalonia.Rendering.SceneGraph { if (_children == null) { - _children = new PooledList(capacity); + _children = new List(capacity); } } @@ -390,7 +382,7 @@ namespace Avalonia.Rendering.SceneGraph { if (_drawOperations == null) { - _drawOperations = new PooledList>(); + _drawOperations = new List>(); _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); _drawOperationsCloned = false; } @@ -398,7 +390,7 @@ namespace Avalonia.Rendering.SceneGraph { var oldDrawOperations = _drawOperations; - _drawOperations = new PooledList>(oldDrawOperations.Count); + _drawOperations = new List>(oldDrawOperations.Count); foreach (var drawOperation in oldDrawOperations) { @@ -418,7 +410,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Draw operations that need to be disposed. /// Disposable for given draw operations. - private static IDisposable CreateDisposeDrawOperations(PooledList> drawOperations) + private static IDisposable CreateDisposeDrawOperations(List> drawOperations) { return Disposable.Create(drawOperations, operations => { @@ -426,8 +418,6 @@ namespace Avalonia.Rendering.SceneGraph { operation.Dispose(); } - - operations.Dispose(); }); } @@ -437,8 +427,6 @@ namespace Avalonia.Rendering.SceneGraph { _drawOperationsRefCounter?.Dispose(); - _children?.Dispose(); - Disposed = true; } } diff --git a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs index 4b3e757e7a..8c4004efdc 100644 --- a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs @@ -377,7 +377,19 @@ namespace Avalonia.VisualTree /// public static bool IsVisualAncestorOf(this IVisual visual, IVisual target) { - return target.GetVisualAncestors().Any(x => x == visual); + IVisual current = target?.VisualParent; + + while (current != null) + { + if (current == visual) + { + return true; + } + + current = current.VisualParent; + } + + return false; } public static IEnumerable SortByZIndex(this IEnumerable elements) diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs index 5c9a0c992a..07da0048d0 100644 --- a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs +++ b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs @@ -102,23 +102,29 @@ namespace Avalonia.X11.NativeDialogs return tcs.Task; } - public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + public async Task ShowFileDialogAsync(FileDialog dialog, Window parent) { await EnsureInitialized(); + + var platformImpl = parent?.PlatformImpl; + return await await RunOnGlibThread( - () => ShowDialog(dialog.Title, parent, + () => ShowDialog(dialog.Title, platformImpl, dialog is OpenFileDialog ? GtkFileChooserAction.Open : GtkFileChooserAction.Save, (dialog as OpenFileDialog)?.AllowMultiple ?? false, Path.Combine(string.IsNullOrEmpty(dialog.InitialDirectory) ? "" : dialog.InitialDirectory, string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName), dialog.Filters)); } - public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) { await EnsureInitialized(); + + var platformImpl = parent?.PlatformImpl; + return await await RunOnGlibThread(async () => { - var res = await ShowDialog(dialog.Title, parent, + var res = await ShowDialog(dialog.Title, platformImpl, GtkFileChooserAction.SelectFolder, false, dialog.InitialDirectory, null); return res?.FirstOrDefault(); }); diff --git a/src/Avalonia.X11/X11Atoms.cs b/src/Avalonia.X11/X11Atoms.cs index db74a32b99..523b65c115 100644 --- a/src/Avalonia.X11/X11Atoms.cs +++ b/src/Avalonia.X11/X11Atoms.cs @@ -156,6 +156,7 @@ namespace Avalonia.X11 public readonly IntPtr _NET_SYSTEM_TRAY_OPCODE; public readonly IntPtr _NET_WM_STATE_MAXIMIZED_HORZ; public readonly IntPtr _NET_WM_STATE_MAXIMIZED_VERT; + public readonly IntPtr _NET_WM_STATE_FULLSCREEN; public readonly IntPtr _XEMBED; public readonly IntPtr _XEMBED_INFO; public readonly IntPtr _MOTIF_WM_HINTS; diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 3a919e2bc4..026a1457a3 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -220,16 +220,11 @@ namespace Avalonia.X11 var decorations = MotifDecorations.Menu | MotifDecorations.Title | MotifDecorations.Border | MotifDecorations.Maximize | MotifDecorations.Minimize | MotifDecorations.ResizeH; - if (_popup || _systemDecorations == SystemDecorations.None) - { + if (_popup + || _systemDecorations == SystemDecorations.None) decorations = 0; - } - else if (_systemDecorations == SystemDecorations.BorderOnly) - { - decorations = MotifDecorations.Border; - } - if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly) + if (!_canResize) { functions &= ~(MotifFunctions.Resize | MotifFunctions.Maximize); decorations &= ~(MotifDecorations.Maximize | MotifDecorations.ResizeH); @@ -252,7 +247,7 @@ namespace Avalonia.X11 var min = _minMaxSize.minSize; var max = _minMaxSize.maxSize; - if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly) + if (!_canResize) max = min = _realSize; if (preResize.HasValue) @@ -552,12 +547,21 @@ namespace Avalonia.X11 else if (value == WindowState.Maximized) { ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_HIDDEN); + ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_FULLSCREEN); ChangeWMAtoms(true, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT, _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ); } + else if (value == WindowState.FullScreen) + { + ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_HIDDEN); + ChangeWMAtoms(true, _x11.Atoms._NET_WM_STATE_FULLSCREEN); + ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT, + _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ); + } else { ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_HIDDEN); + ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_FULLSCREEN); ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT, _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ); } @@ -585,6 +589,12 @@ namespace Avalonia.X11 break; } + if(pitems[c] == _x11.Atoms._NET_WM_STATE_FULLSCREEN) + { + state = WindowState.FullScreen; + break; + } + if (pitems[c] == _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ || pitems[c] == _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT) { @@ -810,7 +820,7 @@ namespace Avalonia.X11 public void SetSystemDecorations(SystemDecorations enabled) { - _systemDecorations = enabled; + _systemDecorations = enabled == SystemDecorations.Full ? SystemDecorations.Full : SystemDecorations.None; UpdateMotifHints(); UpdateSizeHints(null); } @@ -1052,7 +1062,7 @@ namespace Avalonia.X11 void ChangeWMAtoms(bool enable, params IntPtr[] atoms) { - if (atoms.Length < 1 || atoms.Length > 4) + if (atoms.Length != 1 && atoms.Length != 2) throw new ArgumentException(); if (!_mapped) diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs index 871ab77cac..099ffa8c8c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs @@ -4,7 +4,8 @@ using System.Globalization; namespace Avalonia.Markup.Xaml.Converters { - using System.ComponentModel; + using System.ComponentModel; + using Avalonia.Utilities; public class PointsListTypeConverter : TypeConverter { @@ -15,15 +16,17 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { - string strValue = (string)value; - string[] pointStrs = strValue.Split(new[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - var result = new List(pointStrs.Length); - foreach (var pointStr in pointStrs) + var points = new List(); + + using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PointsList.")) { - result.Add(Point.Parse(pointStr)); + while (tokenizer.TryReadDouble(out double x)) + { + points.Add(new Point(x, tokenizer.ReadDouble())); + } } - return result; + return points; } } } 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/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 4bec7b3f56..9f99ed3cef 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -30,6 +30,10 @@ namespace Avalonia.Skia private Matrix _currentTransform; private GRContext _grContext; private bool _disposed; + + private readonly SKPaint _strokePaint = new SKPaint(); + private readonly SKPaint _fillPaint = new SKPaint(); + /// /// Context create info. /// @@ -153,7 +157,7 @@ namespace Avalonia.Skia /// public void DrawLine(IPen pen, Point p1, Point p2) { - using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) + using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) { Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint); } @@ -165,8 +169,8 @@ namespace Avalonia.Skia var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; - using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper)) - using (var stroke = pen?.Brush != null ? CreatePaint(pen, size) : default(PaintWrapper)) + using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper)) + using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper)) { if (fill.Paint != null) { @@ -188,7 +192,7 @@ namespace Avalonia.Skia if (brush != null) { - using (var paint = CreatePaint(brush, rect.Size)) + using (var paint = CreatePaint(_fillPaint, brush, rect.Size)) { if (isRounded) { @@ -204,7 +208,7 @@ namespace Avalonia.Skia if (pen?.Brush != null) { - using (var paint = CreatePaint(pen, rect.Size)) + using (var paint = CreatePaint(_strokePaint, pen, rect.Size)) { if (isRounded) { @@ -222,7 +226,7 @@ namespace Avalonia.Skia /// public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) { - using (var paint = CreatePaint(foreground, text.Bounds.Size)) + using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds.Size)) { var textImpl = (FormattedTextImpl) text; textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering); @@ -232,14 +236,14 @@ namespace Avalonia.Skia /// public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) { - using (var paint = CreatePaint(foreground, glyphRun.Bounds.Size)) + using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Bounds.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; - paint.ApplyTo(glyphRunImpl.Paint); + ConfigureTextRendering(paintWrapper); Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X, - (float)baselineOrigin.Y, glyphRunImpl.Paint); + (float)baselineOrigin.Y, paintWrapper.Paint); } } @@ -323,7 +327,7 @@ namespace Avalonia.Skia var paint = new SKPaint(); Canvas.SaveLayer(paint); - _maskStack.Push(CreatePaint(mask, bounds.Size)); + _maskStack.Push(CreatePaint(paint, mask, bounds.Size, true)); } /// @@ -364,6 +368,15 @@ namespace Avalonia.Skia } } + internal void ConfigureTextRendering(PaintWrapper wrapper) + { + var paint = wrapper.Paint; + + paint.IsEmbeddedBitmapText = true; + paint.SubpixelText = true; + paint.LcdRenderText = _canTextUseLcdRendering; + } + /// /// Configure paint wrapper for using gradient brush. /// @@ -514,17 +527,16 @@ namespace Avalonia.Skia /// /// Creates paint wrapper for given brush. /// + /// The paint to wrap. /// Source brush. /// Target size. + /// Optional dispose of the supplied paint. /// Paint wrapper for given brush. - internal PaintWrapper CreatePaint(IBrush brush, Size targetSize) + internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize, bool disposePaint = false) { - var paint = new SKPaint - { - IsAntialias = true - }; + var paintWrapper = new PaintWrapper(paint, disposePaint); - var paintWrapper = new PaintWrapper(paint); + paint.IsAntialias = true; double opacity = brush.Opacity * _currentOpacity; @@ -572,10 +584,12 @@ namespace Avalonia.Skia /// /// Creates paint wrapper for given pen. /// + /// The paint to wrap. /// Source pen. /// Target size. + /// Optional dispose of the supplied paint. /// - private PaintWrapper CreatePaint(IPen pen, Size targetSize) + private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Size targetSize, bool disposePaint = false) { // In Skia 0 thickness means - use hairline rendering // and for us it means - there is nothing rendered. @@ -584,8 +598,7 @@ namespace Avalonia.Skia return default; } - var rv = CreatePaint(pen.Brush, targetSize); - var paint = rv.Paint; + var rv = CreatePaint(paint, pen.Brush, targetSize, disposePaint); paint.IsStroke = true; paint.StrokeWidth = (float) pen.Thickness; @@ -668,7 +681,7 @@ namespace Avalonia.Skia /// /// Skia cached paint state. /// - private struct PaintState : IDisposable + private readonly struct PaintState : IDisposable { private readonly SKColor _color; private readonly SKShader _shader; @@ -696,14 +709,16 @@ namespace Avalonia.Skia { //We are saving memory allocations there public readonly SKPaint Paint; + private readonly bool _disposePaint; private IDisposable _disposable1; private IDisposable _disposable2; private IDisposable _disposable3; - public PaintWrapper(SKPaint paint) + public PaintWrapper(SKPaint paint, bool disposePaint) { Paint = paint; + _disposePaint = disposePaint; _disposable1 = null; _disposable2 = null; @@ -751,7 +766,15 @@ namespace Avalonia.Skia /// public void Dispose() { - Paint?.Dispose(); + if (_disposePaint) + { + Paint?.Dispose(); + } + else + { + Paint?.Reset(); + } + _disposable1?.Dispose(); _disposable2?.Dispose(); _disposable3?.Dispose(); diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 53aa6a147c..415a89e1c1 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -32,6 +32,27 @@ namespace Avalonia.Skia public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { + SKFontStyle skFontStyle; + + switch (fontWeight) + { + case FontWeight.Normal when fontStyle == FontStyle.Normal: + skFontStyle = SKFontStyle.Normal; + break; + case FontWeight.Normal when fontStyle == FontStyle.Italic: + skFontStyle = SKFontStyle.Italic; + break; + case FontWeight.Bold when fontStyle == FontStyle.Normal: + skFontStyle = SKFontStyle.Bold; + break; + case FontWeight.Bold when fontStyle == FontStyle.Italic: + skFontStyle = SKFontStyle.BoldItalic; + break; + default: + skFontStyle = new SKFontStyle((SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle); + break; + } + if (culture == null) { culture = CultureInfo.CurrentUICulture; @@ -45,31 +66,32 @@ namespace Avalonia.Skia t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName; t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName; - if (fontFamily != null) + if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks) { - foreach (var familyName in fontFamily.FamilyNames) + var familyNames = fontFamily.FamilyNames; + + for (var i = 1; i < familyNames.Count; i++) { - var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight, - SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint); + var skTypeface = + _skFontManager.MatchCharacter(familyNames[i], skFontStyle, t_languageTagBuffer, codepoint); if (skTypeface == null) { continue; } - fontKey = new FontKey(new FontFamily(familyName), fontWeight, fontStyle); + fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle); return true; } } else { - var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, - SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint); + var skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint); if (skTypeface != null) { - fontKey = new FontKey(new FontFamily(skTypeface.FamilyName), fontWeight, fontStyle); + fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle); return true; } @@ -82,7 +104,7 @@ namespace Avalonia.Skia public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) { - var skTypeface = SKTypeface.Default; + SKTypeface skTypeface = null; if (typeface.FontFamily.Key == null) { @@ -109,6 +131,12 @@ namespace Avalonia.Skia skTypeface = fontCollection.Get(typeface); } + if (skTypeface == null) + { + throw new InvalidOperationException( + $"Could not create glyph typeface for: {typeface.FontFamily.Name}."); + } + return new GlyphTypefaceImpl(skTypeface); } } diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index d0157815a9..5f876464e2 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -149,7 +149,17 @@ namespace Avalonia.Skia if (index >= Text.Length || index < 0) { var r = rects.LastOrDefault(); - return new Rect(r.X + r.Width, r.Y, 0, _lineHeight); + + var c = Text[Text.Length - 1]; + + switch (c) + { + case '\n': + case '\r': + return new Rect(r.X, r.Y, 0, _lineHeight); + default: + return new Rect(r.X + r.Width, r.Y, 0, _lineHeight); + } } return rects[index]; } @@ -266,7 +276,8 @@ namespace Avalonia.Skia if (fb != null) { //TODO: figure out how to get the brush size - currentWrapper = context.CreatePaint(fb, new Size()); + currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb, + new Size()); } else { diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index 7c3f718f9e..0fdea5ed40 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -7,17 +7,11 @@ namespace Avalonia.Skia /// public class GlyphRunImpl : IGlyphRunImpl { - public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob) + public GlyphRunImpl(SKTextBlob textBlob) { - Paint = paint; TextBlob = textBlob; } - /// - /// Gets the paint to draw with. - /// - public SKPaint Paint { get; } - /// /// Gets the text blob to draw. /// @@ -26,7 +20,6 @@ namespace Avalonia.Skia void IDisposable.Dispose() { TextBlob.Dispose(); - Paint.Dispose(); } } } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index f1df6a804e..d277267d5d 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -18,7 +18,7 @@ namespace Avalonia.Skia private GRContext GrContext { get; } - public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu) + public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu, long maxResourceBytes = 100663296) { if (customSkiaGpu != null) { @@ -26,6 +26,10 @@ namespace Avalonia.Skia GrContext = _customSkiaGpu.GrContext; + GrContext.GetResourceCacheLimits(out var maxResources, out _); + + GrContext.SetResourceCacheLimits(maxResources, maxResourceBytes); + return; } @@ -39,6 +43,10 @@ namespace Avalonia.Skia : GRGlInterface.AssembleGlesInterface((_, proc) => display.GlInterface.GetProcAddress(proc))) { GrContext = GRContext.Create(GRBackend.OpenGL, iface); + + GrContext.GetResourceCacheLimits(out var maxResources, out _); + + GrContext.SetResourceCacheLimits(maxResources, maxResourceBytes); } display.ClearContext(); } @@ -149,6 +157,16 @@ namespace Avalonia.Skia return new WriteableBitmapImpl(size, dpi, format); } + private static readonly SKPaint s_paint = new SKPaint + { + TextEncoding = SKTextEncoding.GlyphId, + IsAntialias = true, + IsStroke = false, + SubpixelText = true + }; + + private static readonly SKTextBlobBuilder s_textBlobBuilder = new SKTextBlobBuilder(); + /// public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) { @@ -158,92 +176,84 @@ namespace Avalonia.Skia var typeface = glyphTypeface.Typeface; - var paint = new SKPaint - { - TextSize = (float)glyphRun.FontRenderingEmSize, - Typeface = typeface, - TextEncoding = SKTextEncoding.GlyphId, - IsAntialias = true, - IsStroke = false, - SubpixelText = true - }; + s_paint.TextSize = (float)glyphRun.FontRenderingEmSize; + s_paint.Typeface = typeface; - using (var textBlobBuilder = new SKTextBlobBuilder()) - { - SKTextBlob textBlob; - width = 0; + SKTextBlob textBlob; - var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight); + width = 0; - if (glyphRun.GlyphOffsets.IsEmpty) - { - if (glyphTypeface.IsFixedPitch) - { - textBlobBuilder.AddRun(paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span); - - textBlob = textBlobBuilder.Build(); - - width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length; - } - else - { - var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0); - - var positions = buffer.GetPositionSpan(); + var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight); - for (var i = 0; i < count; i++) - { - positions[i] = (float)width; - - if (glyphRun.GlyphAdvances.IsEmpty) - { - width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; - } - else - { - width += glyphRun.GlyphAdvances[i]; - } - } + if (glyphRun.GlyphOffsets.IsEmpty) + { + if (glyphTypeface.IsFixedPitch) + { + s_textBlobBuilder.AddRun(s_paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span); - buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span); + textBlob = s_textBlobBuilder.Build(); - textBlob = textBlobBuilder.Build(); - } + width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length; } else { - var buffer = textBlobBuilder.AllocatePositionedRun(paint, count); - - var glyphPositions = buffer.GetPositionSpan(); + var buffer = s_textBlobBuilder.AllocateHorizontalRun(s_paint, count, 0); - var currentX = 0.0; + var positions = buffer.GetPositionSpan(); for (var i = 0; i < count; i++) { - var glyphOffset = glyphRun.GlyphOffsets[i]; - - glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y); + positions[i] = (float)width; if (glyphRun.GlyphAdvances.IsEmpty) { - currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; + width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; } else { - currentX += glyphRun.GlyphAdvances[i]; + width += glyphRun.GlyphAdvances[i]; } } buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span); - width = currentX; + textBlob = s_textBlobBuilder.Build(); + } + } + else + { + var buffer = s_textBlobBuilder.AllocatePositionedRun(s_paint, count); + + var glyphPositions = buffer.GetPositionSpan(); + + var currentX = 0.0; + + for (var i = 0; i < count; i++) + { + var glyphOffset = glyphRun.GlyphOffsets[i]; - textBlob = textBlobBuilder.Build(); + glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y); + + if (glyphRun.GlyphAdvances.IsEmpty) + { + currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; + } + else + { + currentX += glyphRun.GlyphAdvances[i]; + } } - return new GlyphRunImpl(paint, textBlob); + buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span); + + width = currentX; + + textBlob = s_textBlobBuilder.Build(); } + + return new GlyphRunImpl(textBlob); + } } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index ce82835968..7aea90e61e 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -19,7 +19,7 @@ namespace Avalonia.Skia public SKTypeface Get(Typeface typeface) { - var key = new FontKey(typeface.FontFamily, typeface.Weight, typeface.Style); + var key = new FontKey(typeface.FontFamily.Name, typeface.Weight, typeface.Style); return GetNearestMatch(_typefaces, key); } @@ -49,7 +49,7 @@ namespace Avalonia.Skia if (keys.Length == 0) { - return SKTypeface.Default; + return null; } key = keys[0]; diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index 4f04d25dee..a9aed80a04 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -54,7 +54,7 @@ namespace Avalonia.Skia continue; } - var key = new FontKey(fontFamily, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant); + var key = new FontKey(fontFamily.Name, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant); typeFaceCollection.AddTypeface(key, typeface); } diff --git a/src/Skia/Avalonia.Skia/SkiaOptions.cs b/src/Skia/Avalonia.Skia/SkiaOptions.cs index f4b8fe3c1d..2d5e5990e6 100644 --- a/src/Skia/Avalonia.Skia/SkiaOptions.cs +++ b/src/Skia/Avalonia.Skia/SkiaOptions.cs @@ -8,9 +8,18 @@ namespace Avalonia /// public class SkiaOptions { + public SkiaOptions() + { + MaxGpuResourceSizeBytes = 100663296; // Value taken from skia. + } /// /// Custom gpu factory to use. Can be used to customize behavior of Skia renderer. /// public Func CustomGpuFactory { get; set; } + + /// + /// The maximum number of bytes for video memory to store textures and resources. + /// + public long MaxGpuResourceSizeBytes { get; set; } } } diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs index b4b340d24d..9a5725e06f 100644 --- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs +++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs @@ -18,7 +18,7 @@ namespace Avalonia.Skia public static void Initialize(SkiaOptions options) { var customGpu = options.CustomGpuFactory?.Invoke(); - var renderInterface = new PlatformRenderInterface(customGpu); + var renderInterface = new PlatformRenderInterface(customGpu, options.MaxGpuResourceSizeBytes); AvaloniaLocator.CurrentMutable .Bind().ToConstant(renderInterface) diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index 8ba46dcbce..253a373106 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -50,7 +50,7 @@ namespace Avalonia.Direct2D1.Media var fontFamilyName = font.FontFamily.FamilyNames.GetString(0); - fontKey = new FontKey(new FontFamily(fontFamilyName), fontWeight, fontStyle); + fontKey = new FontKey(fontFamilyName, fontWeight, fontStyle); return true; } diff --git a/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs b/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs new file mode 100644 index 0000000000..1b01ebbe7f --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs @@ -0,0 +1,55 @@ +using System; +using System.Runtime.InteropServices; +using static Avalonia.Win32.Interop.UnmanagedMethods; + +namespace Avalonia.Win32.Interop +{ + internal class TaskBarList + { + private static IntPtr s_taskBarList; + private static HrInit s_hrInitDelegate; + private static MarkFullscreenWindow s_markFullscreenWindowDelegate; + + /// + /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc + /// + /// Fullscreen state. + public static unsafe void MarkFullscreen(IntPtr hwnd, bool fullscreen) + { + if (s_taskBarList == IntPtr.Zero) + { + Guid clsid = ShellIds.TaskBarList; + Guid iid = ShellIds.ITaskBarList2; + + int result = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out s_taskBarList); + + if (s_taskBarList != IntPtr.Zero) + { + var ptr = (ITaskBarList2VTable**)s_taskBarList.ToPointer(); + + if (s_hrInitDelegate is null) + { + s_hrInitDelegate = Marshal.GetDelegateForFunctionPointer((*ptr)->HrInit); + } + + if (s_hrInitDelegate(s_taskBarList) != HRESULT.S_OK) + { + s_taskBarList = IntPtr.Zero; + } + } + } + + if (s_taskBarList != IntPtr.Zero) + { + var ptr = (ITaskBarList2VTable**)s_taskBarList.ToPointer(); + + if (s_markFullscreenWindowDelegate is null) + { + s_markFullscreenWindowDelegate = Marshal.GetDelegateForFunctionPointer((*ptr)->MarkFullscreenWindow); + } + + s_markFullscreenWindowDelegate(s_taskBarList, hwnd, fullscreen); + } + } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index cbfa1abfb7..5601ccbafe 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -460,6 +460,7 @@ namespace Avalonia.Win32.Interop WS_SIZEFRAME = 0x40000, WS_SYSMENU = 0x80000, WS_TABSTOP = 0x10000, + WS_THICKFRAME = 0x40000, WS_VISIBLE = 0x10000000, WS_VSCROLL = 0x200000, WS_EX_DLGMODALFRAME = 0x00000001, @@ -1146,7 +1147,10 @@ namespace Avalonia.Win32.Interop internal static extern int CoCreateInstance(ref Guid clsid, IntPtr ignore1, int ignore2, ref Guid iid, [MarshalAs(UnmanagedType.IUnknown), Out] out object pUnkOuter); - + [DllImport("ole32.dll", PreserveSig = true)] + internal static extern int CoCreateInstance(ref Guid clsid, + IntPtr ignore1, int ignore2, ref Guid iid, [Out] out IntPtr pUnkOuter); + [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IntPtr pbc, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppv); @@ -1642,6 +1646,8 @@ namespace Avalonia.Win32.Interop public static readonly Guid SaveFileDialog = Guid.Parse("C0B4E2F3-BA21-4773-8DBA-335EC946EB8B"); public static readonly Guid IFileDialog = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8"); public static readonly Guid IShellItem = Guid.Parse("43826D1E-E718-42EE-BC55-A1E261C37BFE"); + public static readonly Guid TaskBarList = Guid.Parse("56FDF344-FD6D-11D0-958A-006097C9A090"); + public static readonly Guid ITaskBarList2 = Guid.Parse("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf"); } [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] @@ -1874,6 +1880,22 @@ namespace Avalonia.Win32.Interop [MarshalAs(UnmanagedType.LPWStr)] public string pszSpec; } + + public delegate void MarkFullscreenWindow(IntPtr This, IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fullscreen); + public delegate HRESULT HrInit(IntPtr This); + + public struct ITaskBarList2VTable + { + public IntPtr IUnknown1; + public IntPtr IUnknown2; + public IntPtr IUnknown3; + public IntPtr HrInit; + public IntPtr AddTab; + public IntPtr DeleteTab; + public IntPtr ActivateTab; + public IntPtr SetActiveAlt; + public IntPtr MarkFullscreenWindow; + } } [Flags] diff --git a/src/Windows/Avalonia.Win32/ScreenImpl.cs b/src/Windows/Avalonia.Win32/ScreenImpl.cs index 963042b249..442794f0f0 100644 --- a/src/Windows/Avalonia.Win32/ScreenImpl.cs +++ b/src/Windows/Avalonia.Win32/ScreenImpl.cs @@ -8,7 +8,7 @@ namespace Avalonia.Win32 { public class ScreenImpl : IScreenImpl { - public int ScreenCount + public int ScreenCount { get => GetSystemMetrics(SystemMetric.SM_CMONITORS); } @@ -33,7 +33,7 @@ namespace Avalonia.Win32 var shcore = LoadLibrary("shcore.dll"); var method = GetProcAddress(shcore, nameof(GetDpiForMonitor)); if (method != IntPtr.Zero) - { + { GetDpiForMonitor(monitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var x, out _); dpi = (double)x; } @@ -51,11 +51,8 @@ namespace Avalonia.Win32 RECT bounds = monitorInfo.rcMonitor; RECT workingArea = monitorInfo.rcWork; - PixelRect avaloniaBounds = new PixelRect(bounds.left, bounds.top, bounds.right - bounds.left, - bounds.bottom - bounds.top); - PixelRect avaloniaWorkArea = - new PixelRect(workingArea.left, workingArea.top, workingArea.right - workingArea.left, - workingArea.bottom - workingArea.top); + PixelRect avaloniaBounds = bounds.ToPixelRect(); + PixelRect avaloniaWorkArea = workingArea.ToPixelRect(); screens[index] = new WinScreen(dpi / 96.0d, avaloniaBounds, avaloniaWorkArea, monitorInfo.dwFlags == 1, monitor); diff --git a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs index c452290afc..c6164e0868 100644 --- a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs +++ b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs @@ -5,7 +5,6 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; -using Avalonia.Platform; using Avalonia.Win32.Interop; namespace Avalonia.Win32 @@ -16,16 +15,16 @@ namespace Avalonia.Win32 private const UnmanagedMethods.FOS DefaultDialogOptions = UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT; - public unsafe Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + public unsafe Task ShowFileDialogAsync(FileDialog dialog, Window parent) { - var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero; + var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero; return Task.Factory.StartNew(() => { var result = Array.Empty(); Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog; Guid iid = UnmanagedMethods.ShellIds.IFileDialog; - UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk); + UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out object unk); var frm = (UnmanagedMethods.IFileDialog)unk; var openDialog = dialog as OpenFileDialog; @@ -98,17 +97,17 @@ namespace Avalonia.Win32 }); } - public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) { return Task.Factory.StartNew(() => { string result = string.Empty; - var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero; + var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero; Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog; - Guid iid = UnmanagedMethods.ShellIds.IFileDialog; + Guid iid = UnmanagedMethods.ShellIds.IFileDialog; - UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk); + UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out object unk); var frm = (UnmanagedMethods.IFileDialog)unk; uint options; frm.GetOptions(out options); diff --git a/src/Windows/Avalonia.Win32/Win32TypeExtensions.cs b/src/Windows/Avalonia.Win32/Win32TypeExtensions.cs new file mode 100644 index 0000000000..8193611f6d --- /dev/null +++ b/src/Windows/Avalonia.Win32/Win32TypeExtensions.cs @@ -0,0 +1,13 @@ +using static Avalonia.Win32.Interop.UnmanagedMethods; + +namespace Avalonia.Win32 +{ + internal static class Win32TypeExtensions + { + public static PixelRect ToPixelRect(this RECT rect) + { + return new PixelRect(rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top); + } + } +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index e193c72ef7..d82e9f64f1 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -37,10 +37,14 @@ namespace Avalonia.Win32 { WindowEdge.West, HitTestValues.HTLEFT } }; + private SavedWindowInfo _savedWindowInfo; + private bool _isFullScreenActive; + #if USE_MANAGED_DRAG private readonly ManagedWindowResizeDragHelper _managedDrag; #endif + private const WindowStyles WindowStateMask = (WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE); private readonly List _disabledBy; private readonly TouchDevice _touchDevice; private readonly MouseDevice _mouseDevice; @@ -82,7 +86,9 @@ namespace Avalonia.Win32 _windowProperties = new WindowProperties { - ShowInTaskbar = false, IsResizable = true, Decorations = SystemDecorations.Full + ShowInTaskbar = false, + IsResizable = true, + Decorations = SystemDecorations.Full }; _rendererLock = new ManagedDeferredRendererLock(); @@ -538,27 +544,98 @@ namespace Avalonia.Win32 } } + /// + /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc + /// Method must only be called from inside UpdateWindowProperties. + /// + /// + private void SetFullScreen(bool fullscreen) + { + if (fullscreen) + { + GetWindowRect(_hwnd, out var windowRect); + _savedWindowInfo.WindowRect = windowRect; + + var current = GetStyle(); + var currentEx = GetExtendedStyle(); + + _savedWindowInfo.Style = current; + _savedWindowInfo.ExStyle = currentEx; + + // Set new window style and size. + SetStyle(current & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME), false); + SetExtendedStyle(currentEx & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE), false); + + // On expand, if we're given a window_rect, grow to it, otherwise do + // not resize. + MONITORINFO monitor_info = MONITORINFO.Create(); + GetMonitorInfo(MonitorFromWindow(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST), ref monitor_info); + + var window_rect = monitor_info.rcMonitor.ToPixelRect(); + + SetWindowPos(_hwnd, IntPtr.Zero, window_rect.X, window_rect.Y, + window_rect.Width, window_rect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); + + _isFullScreenActive = true; + } + else + { + // Reset original window style and size. The multiple window size/moves + // here are ugly, but if SetWindowPos() doesn't redraw, the taskbar won't be + // repainted. Better-looking methods welcome. + _isFullScreenActive = false; + + var windowStates = GetWindowStateStyles(); + SetStyle((_savedWindowInfo.Style & ~WindowStateMask) | windowStates, false); + SetExtendedStyle(_savedWindowInfo.ExStyle, false); + + // On restore, resize to the previous saved rect size. + var new_rect = _savedWindowInfo.WindowRect.ToPixelRect(); + + SetWindowPos(_hwnd, IntPtr.Zero, new_rect.X, new_rect.Y, new_rect.Width, + new_rect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); + + UpdateWindowProperties(_windowProperties, true); + } + + TaskBarList.MarkFullscreen(_hwnd, fullscreen); + } + private void ShowWindow(WindowState state) { ShowWindowCommand command; + var newWindowProperties = _windowProperties; + switch (state) { case WindowState.Minimized: + newWindowProperties.IsFullScreen = false; command = ShowWindowCommand.Minimize; break; case WindowState.Maximized: + newWindowProperties.IsFullScreen = false; command = ShowWindowCommand.Maximize; break; case WindowState.Normal: + newWindowProperties.IsFullScreen = false; command = ShowWindowCommand.Restore; break; + case WindowState.FullScreen: + newWindowProperties.IsFullScreen = true; + UpdateWindowProperties(newWindowProperties); + return; + default: throw new ArgumentException("Invalid WindowState."); } + UpdateWindowProperties(newWindowProperties); + UnmanagedMethods.ShowWindow(_hwnd, command); if (state == WindowState.Maximized) @@ -590,22 +667,69 @@ namespace Avalonia.Win32 SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); } } + } + + private WindowStyles GetWindowStateStyles () + { + return GetStyle() & WindowStateMask; } - private WindowStyles GetStyle() => (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE); + private WindowStyles GetStyle() + { + if (_isFullScreenActive) + { + return _savedWindowInfo.Style; + } + else + { + return (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE); + } + } - private WindowStyles GetExtendedStyle() => (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE); + private WindowStyles GetExtendedStyle() + { + if (_isFullScreenActive) + { + return _savedWindowInfo.ExStyle; + } + else + { + return (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE); + } + } - private void SetStyle(WindowStyles style) => SetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE, (uint)style); + private void SetStyle(WindowStyles style, bool save = true) + { + if (save) + { + _savedWindowInfo.Style = style; + } - private void SetExtendedStyle(WindowStyles style) => SetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE, (uint)style); + if (!_isFullScreenActive) + { + SetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE, (uint)style); + } + } + + private void SetExtendedStyle(WindowStyles style, bool save = true) + { + if (save) + { + _savedWindowInfo.ExStyle = style; + } + + if (!_isFullScreenActive) + { + SetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE, (uint)style); + } + } private void UpdateEnabled() { EnableWindow(_hwnd, _disabledBy.Count == 0); } - private void UpdateWindowProperties(WindowProperties newProperties) + private void UpdateWindowProperties(WindowProperties newProperties, bool forceChanges = false) { var oldProperties = _windowProperties; @@ -613,7 +737,7 @@ namespace Avalonia.Win32 // according to the new values already. _windowProperties = newProperties; - if (oldProperties.ShowInTaskbar != newProperties.ShowInTaskbar) + if ((oldProperties.ShowInTaskbar != newProperties.ShowInTaskbar) || forceChanges) { var exStyle = GetExtendedStyle(); @@ -632,7 +756,7 @@ namespace Avalonia.Win32 // Otherwise it will still show in the taskbar. } - if (oldProperties.IsResizable != newProperties.IsResizable) + if ((oldProperties.IsResizable != newProperties.IsResizable) || forceChanges) { var style = GetStyle(); @@ -648,7 +772,12 @@ namespace Avalonia.Win32 SetStyle(style); } - if (oldProperties.Decorations != newProperties.Decorations) + if (oldProperties.IsFullScreen != newProperties.IsFullScreen) + { + SetFullScreen(newProperties.IsFullScreen); + } + + if ((oldProperties.Decorations != newProperties.Decorations) || forceChanges) { var style = GetStyle(); @@ -663,30 +792,33 @@ namespace Avalonia.Win32 style &= ~fullDecorationFlags; } - var margins = new MARGINS + SetStyle(style); + + if (!_isFullScreenActive) { - cyBottomHeight = newProperties.Decorations == SystemDecorations.BorderOnly ? 1 : 0 - }; + var margins = new MARGINS + { + cyBottomHeight = newProperties.Decorations == SystemDecorations.BorderOnly ? 1 : 0 + }; - DwmExtendFrameIntoClientArea(_hwnd, ref margins); + DwmExtendFrameIntoClientArea(_hwnd, ref margins); - GetClientRect(_hwnd, out var oldClientRect); - var oldClientRectOrigin = new POINT(); - ClientToScreen(_hwnd, ref oldClientRectOrigin); - oldClientRect.Offset(oldClientRectOrigin); + GetClientRect(_hwnd, out var oldClientRect); + var oldClientRectOrigin = new POINT(); + ClientToScreen(_hwnd, ref oldClientRectOrigin); + oldClientRect.Offset(oldClientRectOrigin); - SetStyle(style); + var newRect = oldClientRect; - var newRect = oldClientRect; + if (newProperties.Decorations == SystemDecorations.Full) + { + AdjustWindowRectEx(ref newRect, (uint)style, false, (uint)GetExtendedStyle()); + } - if (newProperties.Decorations == SystemDecorations.Full) - { - AdjustWindowRectEx(ref newRect, (uint)style, false, (uint)GetExtendedStyle()); + SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | + SetWindowPosFlags.SWP_FRAMECHANGED); } - - SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height, - SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | - SetWindowPosFlags.SWP_FRAMECHANGED); } } @@ -713,11 +845,19 @@ namespace Avalonia.Win32 IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle; + private struct SavedWindowInfo + { + public WindowStyles Style { get; set; } + public WindowStyles ExStyle { get; set; } + public RECT WindowRect { get; set; } + }; + private struct WindowProperties { public bool ShowInTaskbar; public bool IsResizable; public SystemDecorations Decorations; + public bool IsFullScreen; } } } 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: +