diff --git a/.gitignore b/.gitignore index 7d672c7755..abf7674560 100644 --- a/.gitignore +++ b/.gitignore @@ -106,6 +106,9 @@ _NCrunch_*/ *.ncrunchsolution.user nCrunchTemp_* +# CodeRush +.cr/ + # Others sql/ *.Cache diff --git a/native/Avalonia.Native/inc/comimpl.h b/native/Avalonia.Native/inc/comimpl.h index 0ff64b7215..47b0a3c5f2 100644 --- a/native/Avalonia.Native/inc/comimpl.h +++ b/native/Avalonia.Native/inc/comimpl.h @@ -8,8 +8,109 @@ #include +/** + START_COM_CALL causes AddRef to be called at the beggining of a function. + When a function is exited, it causes ReleaseRef to be called. + This ensures that the object cannot be destroyed whilst the function is running. + For example: Window Show is called, which triggers an event, and user calls Close inside the event + causing the refcount to reach 0, and the object to be destroyed. Function then continues and this pointer + will now be invalid. + + START_COM_CALL protects against this scenario. + */ +#define START_COM_CALL auto r = this->UnknownSelf() + __IID_DEF(IUnknown, 0, 0, 0, C0, 00, 00, 00, 00, 00, 00, 46); +template +class ComPtr +{ +private: + TInterface* _obj; +public: + ComPtr() + { + _obj = 0; + } + + ComPtr(TInterface* pObj) + { + _obj = 0; + + if (pObj) + { + _obj = pObj; + _obj->AddRef(); + } + } + + ComPtr(const ComPtr& ptr) + { + _obj = 0; + + if (ptr._obj) + { + _obj = ptr._obj; + _obj->AddRef(); + } + + } + + ComPtr& operator=(ComPtr other) + { + if(_obj != NULL) + _obj->Release(); + _obj = other._obj; + if(_obj != NULL) + _obj->AddRef(); + return *this; + } + + ~ComPtr() + { + if (_obj) + { + _obj->Release(); + _obj = 0; + } + } + + TInterface* getRaw() + { + return _obj; + } + + TInterface* getRetainedReference() + { + if(_obj == NULL) + return NULL; + _obj->AddRef(); + return _obj; + } + + TInterface** getPPV() + { + return &_obj; + } + + operator TInterface*() const + { + return _obj; + } + TInterface& operator*() const + { + return *_obj; + } + TInterface** operator&() + { + return &_obj; + } + TInterface* operator->() const + { + return _obj; + } +}; + class ComObject : public virtual IUnknown { private: @@ -58,6 +159,12 @@ public: _refCount++; return S_OK; } + +protected: + ComPtr UnknownSelf() + { + return this; + } }; @@ -104,94 +211,5 @@ public: virtual ~ComSingleObject(){} }; -template -class ComPtr -{ -private: - TInterface* _obj; -public: - ComPtr() - { - _obj = 0; - } - - ComPtr(TInterface* pObj) - { - _obj = 0; - - if (pObj) - { - _obj = pObj; - _obj->AddRef(); - } - } - - ComPtr(const ComPtr& ptr) - { - _obj = 0; - - if (ptr._obj) - { - _obj = ptr._obj; - _obj->AddRef(); - } - - } - - ComPtr& operator=(ComPtr other) - { - if(_obj != NULL) - _obj->Release(); - _obj = other._obj; - if(_obj != NULL) - _obj->AddRef(); - return *this; - } - - ~ComPtr() - { - if (_obj) - { - _obj->Release(); - _obj = 0; - } - } - - TInterface* getRaw() - { - return _obj; - } - - TInterface* getRetainedReference() - { - if(_obj == NULL) - return NULL; - _obj->AddRef(); - return _obj; - } - - TInterface** getPPV() - { - return &_obj; - } - - operator TInterface*() const - { - return _obj; - } - TInterface& operator*() const - { - return *_obj; - } - TInterface** operator&() - { - return &_obj; - } - TInterface* operator->() const - { - return _obj; - } -}; - #endif // COMIMPL_H_INCLUDED #pragma clang diagnostic pop diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index 001cf151d8..cd0e2cdf94 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -43,6 +43,8 @@ public: virtual HRESULT Pointer(void**retOut) override { + START_COM_CALL; + @autoreleasepool { if(retOut == nullptr) @@ -58,14 +60,19 @@ public: virtual HRESULT Length(int*retOut) override { - if(retOut == nullptr) + START_COM_CALL; + + @autoreleasepool { - return E_POINTER; + if(retOut == nullptr) + { + return E_POINTER; + } + + *retOut = _length; + + return S_OK; } - - *retOut = _length; - - return S_OK; } }; @@ -109,10 +116,15 @@ public: virtual HRESULT Get(unsigned int index, IAvnString**ppv) override { - if(_list.size() <= index) - return E_INVALIDARG; - *ppv = _list[index].getRetainedReference(); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + if(_list.size() <= index) + return E_INVALIDARG; + *ppv = _list[index].getRetainedReference(); + return S_OK; + } } }; diff --git a/native/Avalonia.Native/src/OSX/Screens.mm b/native/Avalonia.Native/src/OSX/Screens.mm index 10f698ff45..b9c75ed742 100644 --- a/native/Avalonia.Native/src/OSX/Screens.mm +++ b/native/Avalonia.Native/src/OSX/Screens.mm @@ -8,6 +8,8 @@ class Screens : public ComSingleObject public: virtual HRESULT GetScreenCount (int* ret) override { + START_COM_CALL; + @autoreleasepool { *ret = (int)[NSScreen screens].count; @@ -18,6 +20,8 @@ public: virtual HRESULT GetScreen (int index, AvnScreen* ret) override { + START_COM_CALL; + @autoreleasepool { if(index < 0 || index >= [NSScreen screens].count) diff --git a/native/Avalonia.Native/src/OSX/cgl.mm b/native/Avalonia.Native/src/OSX/cgl.mm index a9d94cdf04..085037978e 100644 --- a/native/Avalonia.Native/src/OSX/cgl.mm +++ b/native/Avalonia.Native/src/OSX/cgl.mm @@ -69,6 +69,8 @@ public: virtual HRESULT LegacyMakeCurrent() override { + START_COM_CALL; + if(CGLSetCurrentContext(Context) != 0) return E_FAIL; return S_OK; @@ -76,6 +78,8 @@ public: virtual HRESULT MakeCurrent(IUnknown** ppv) override { + START_COM_CALL; + CGLContextObj saved = CGLGetCurrentContext(); CGLLockContext(Context); if(CGLSetCurrentContext(Context) != 0) @@ -128,6 +132,8 @@ public: virtual HRESULT CreateContext(IAvnGlContext* share, IAvnGlContext**ppv) override { + START_COM_CALL; + CGLContextObj shareContext = nil; if(share != nil) { @@ -144,6 +150,8 @@ public: virtual HRESULT WrapContext(void* native, IAvnGlContext**ppv) override { + START_COM_CALL; + if(native == nil) return E_INVALIDARG; *ppv = new AvnGlContext((CGLContextObj) native); diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index f148374759..9966971b73 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -25,6 +25,8 @@ public: virtual HRESULT GetText (char* type, IAvnString**ppv) override { + START_COM_CALL; + @autoreleasepool { if(ppv == nullptr) @@ -42,6 +44,8 @@ public: virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) override { + START_COM_CALL; + @autoreleasepool { *ppv= nil; @@ -69,56 +73,71 @@ public: virtual HRESULT SetText (char* type, char* utf8String) override { - Clear(); + START_COM_CALL; + @autoreleasepool { + Clear(); + auto string = [NSString stringWithUTF8String:(const char*)utf8String]; auto typeString = [NSString stringWithUTF8String:(const char*)type]; if(_item == nil) [_pb setString: string forType: typeString]; else [_item setString: string forType:typeString]; - } - return S_OK; + return S_OK; + } } virtual HRESULT SetBytes(char* type, void* bytes, int len) override { - auto typeString = [NSString stringWithUTF8String:(const char*)type]; - auto data = [NSData dataWithBytes:bytes length:len]; - if(_item == nil) - [_pb setData:data forType:typeString]; - else - [_item setData:data forType:typeString]; - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + auto typeString = [NSString stringWithUTF8String:(const char*)type]; + auto data = [NSData dataWithBytes:bytes length:len]; + if(_item == nil) + [_pb setData:data forType:typeString]; + else + [_item setData:data forType:typeString]; + return S_OK; + } } virtual HRESULT GetBytes(char* type, IAvnString**ppv) override { - *ppv = nil; - auto typeString = [NSString stringWithUTF8String:(const char*)type]; - NSData*data; - @try + START_COM_CALL; + + @autoreleasepool { - if(_item) - data = [_item dataForType:typeString]; - else - data = [_pb dataForType:typeString]; - if(data == nil) + *ppv = nil; + auto typeString = [NSString stringWithUTF8String:(const char*)type]; + NSData*data; + @try + { + if(_item) + data = [_item dataForType:typeString]; + else + data = [_pb dataForType:typeString]; + if(data == nil) + return E_FAIL; + } + @catch(NSException* e) + { return E_FAIL; + } + *ppv = CreateByteArray((void*)data.bytes, (int)data.length); + return S_OK; } - @catch(NSException* e) - { - return E_FAIL; - } - *ppv = CreateByteArray((void*)data.bytes, (int)data.length); - return S_OK; } virtual HRESULT Clear() override { + START_COM_CALL; + @autoreleasepool { if(_item != nil) @@ -128,15 +147,20 @@ public: [_pb clearContents]; [_pb setString:@"" forType:NSPasteboardTypeString]; } - } - return S_OK; + return S_OK; + } } virtual HRESULT ObtainFormats(IAvnStringArray** ppv) override { - *ppv = CreateAvnStringArray(_item == nil ? [_pb types] : [_item types]); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = CreateAvnStringArray(_item == nil ? [_pb types] : [_item types]); + return S_OK; + } } }; diff --git a/native/Avalonia.Native/src/OSX/controlhost.mm b/native/Avalonia.Native/src/OSX/controlhost.mm index 5ee2344ac7..f8e9a3b6d1 100644 --- a/native/Avalonia.Native/src/OSX/controlhost.mm +++ b/native/Avalonia.Native/src/OSX/controlhost.mm @@ -16,11 +16,16 @@ public: virtual HRESULT CreateDefaultChild(void* parent, void** retOut) override { - NSView* view = [NSView new]; - [view setWantsLayer: true]; + START_COM_CALL; - *retOut = (__bridge_retained void*)view; - return S_OK; + @autoreleasepool + { + NSView* view = [NSView new]; + [view setWantsLayer: true]; + + *retOut = (__bridge_retained void*)view; + return S_OK; + } }; virtual IAvnNativeControlHostTopLevelAttachment* CreateAttachment() override @@ -69,32 +74,42 @@ public: virtual HRESULT InitializeWithChildHandle(void* child) override { - if(_child != nil) - return E_FAIL; - _child = (__bridge NSView*)child; - if(_child == nil) - return E_FAIL; - [_holder addSubview:_child]; - [_child setHidden: false]; - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + if(_child != nil) + return E_FAIL; + _child = (__bridge NSView*)child; + if(_child == nil) + return E_FAIL; + [_holder addSubview:_child]; + [_child setHidden: false]; + return S_OK; + } }; virtual HRESULT AttachTo(IAvnNativeControlHost* host) override { - if(host == nil) - { - [_holder removeFromSuperview]; - [_holder setHidden: true]; - } - else + START_COM_CALL; + + @autoreleasepool { - AvnNativeControlHost* chost = dynamic_cast(host); - if(chost == nil || chost->View == nil) - return E_FAIL; - [_holder setHidden:true]; - [chost->View addSubview:_holder]; + if(host == nil) + { + [_holder removeFromSuperview]; + [_holder setHidden: true]; + } + else + { + AvnNativeControlHost* chost = dynamic_cast(host); + if(chost == nil || chost->View == nil) + return E_FAIL; + [_holder setHidden:true]; + [chost->View addSubview:_holder]; + } + return S_OK; } - return S_OK; }; virtual void ShowInBounds(float x, float y, float width, float height) override diff --git a/native/Avalonia.Native/src/OSX/cursor.mm b/native/Avalonia.Native/src/OSX/cursor.mm index 1732d6e71f..dc38294a18 100644 --- a/native/Avalonia.Native/src/OSX/cursor.mm +++ b/native/Avalonia.Native/src/OSX/cursor.mm @@ -53,36 +53,46 @@ public: virtual HRESULT GetCursor (AvnStandardCursorType cursorType, IAvnCursor** retOut) override { - *retOut = s_cursorMap[cursorType]; + START_COM_CALL; - if(*retOut != nullptr) + @autoreleasepool { - (*retOut)->AddRef(); - } + *retOut = s_cursorMap[cursorType]; - return S_OK; + if(*retOut != nullptr) + { + (*retOut)->AddRef(); + } + + return S_OK; + } } virtual HRESULT CreateCustomCursor (void* bitmapData, size_t length, AvnPixelSize hotPixel, IAvnCursor** retOut) override { - if(bitmapData == nullptr || retOut == nullptr) + START_COM_CALL; + + @autoreleasepool { - return E_POINTER; + if(bitmapData == nullptr || retOut == nullptr) + { + return E_POINTER; + } + + NSData *imageData = [NSData dataWithBytes:bitmapData length:length]; + NSImage *image = [[NSImage alloc] initWithData:imageData]; + + + NSPoint hotSpot; + hotSpot.x = hotPixel.Width; + hotSpot.y = hotPixel.Height; + + *retOut = new Cursor([[NSCursor new] initWithImage: image hotSpot: hotSpot]); + + (*retOut)->AddRef(); + + return S_OK; } - - NSData *imageData = [NSData dataWithBytes:bitmapData length:length]; - NSImage *image = [[NSImage alloc] initWithData:imageData]; - - - NSPoint hotSpot; - hotSpot.x = hotPixel.Width; - hotSpot.y = hotPixel.Height; - - *retOut = new Cursor([[NSCursor new] initWithImage: image hotSpot: hotSpot]); - - (*retOut)->AddRef(); - - return S_OK; } }; diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index aaaf381b26..3e152a6125 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -107,27 +107,42 @@ public: virtual HRESULT SetApplicationTitle(char* utf8String) override { - auto appTitle = [NSString stringWithUTF8String: utf8String]; + START_COM_CALL; - [[NSProcessInfo processInfo] setProcessName:appTitle]; - - - SetProcessName(appTitle); - - return S_OK; + @autoreleasepool + { + auto appTitle = [NSString stringWithUTF8String: utf8String]; + + [[NSProcessInfo processInfo] setProcessName:appTitle]; + + + SetProcessName(appTitle); + + return S_OK; + } } virtual HRESULT SetShowInDock(int show) override { - AvnDesiredActivationPolicy = show - ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory; - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + AvnDesiredActivationPolicy = show + ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory; + return S_OK; + } } virtual HRESULT SetDisableDefaultApplicationMenuItems (bool enabled) override { - SetAutoGenerateDefaultAppMenuItems(!enabled); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + SetAutoGenerateDefaultAppMenuItems(!enabled); + return S_OK; + } } }; @@ -165,6 +180,8 @@ public: FORWARD_IUNKNOWN() virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator, IAvnApplicationEvents* events) override { + START_COM_CALL; + _deallocator = deallocator; @autoreleasepool{ [[ThreadingInitializer new] do]; @@ -180,89 +197,154 @@ public: virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv) override { - if(cb == nullptr || ppv == nullptr) - return E_POINTER; - *ppv = CreateAvnWindow(cb, gl); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + if(cb == nullptr || ppv == nullptr) + return E_POINTER; + *ppv = CreateAvnWindow(cb, gl); + return S_OK; + } }; virtual HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv) override { - if(cb == nullptr || ppv == nullptr) - return E_POINTER; + START_COM_CALL; - *ppv = CreateAvnPopup(cb, gl); - return S_OK; + @autoreleasepool + { + if(cb == nullptr || ppv == nullptr) + return E_POINTER; + + *ppv = CreateAvnPopup(cb, gl); + return S_OK; + } } virtual HRESULT CreatePlatformThreadingInterface(IAvnPlatformThreadingInterface** ppv) override { - *ppv = CreatePlatformThreading(); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = CreatePlatformThreading(); + return S_OK; + } } virtual HRESULT CreateSystemDialogs(IAvnSystemDialogs** ppv) override { - *ppv = ::CreateSystemDialogs(); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateSystemDialogs(); + return S_OK; + } } virtual HRESULT CreateScreens (IAvnScreens** ppv) override { - *ppv = ::CreateScreens (); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateScreens (); + return S_OK; + } } virtual HRESULT CreateClipboard(IAvnClipboard** ppv) override { - *ppv = ::CreateClipboard (nil, nil); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateClipboard (nil, nil); + return S_OK; + } } virtual HRESULT CreateDndClipboard(IAvnClipboard** ppv) override { - *ppv = ::CreateClipboard (nil, [NSPasteboardItem new]); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateClipboard (nil, [NSPasteboardItem new]); + return S_OK; + } } virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) override { - *ppv = ::CreateCursorFactory(); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateCursorFactory(); + return S_OK; + } } virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) override { - auto rv = ::GetGlDisplay(); - if(rv == NULL) - return E_FAIL; - rv->AddRef(); - *ppv = rv; - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + auto rv = ::GetGlDisplay(); + if(rv == NULL) + return E_FAIL; + rv->AddRef(); + *ppv = rv; + return S_OK; + } } virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) override { - *ppv = ::CreateAppMenu(cb); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateAppMenu(cb); + return S_OK; + } } virtual HRESULT CreateMenuItem (IAvnMenuItem** ppv) override { - *ppv = ::CreateAppMenuItem(); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateAppMenuItem(); + return S_OK; + } } virtual HRESULT CreateMenuItemSeparator (IAvnMenuItem** ppv) override { - *ppv = ::CreateAppMenuItemSeparator(); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateAppMenuItemSeparator(); + return S_OK; + } } virtual HRESULT SetAppMenu (IAvnMenu* appMenu) override { - ::SetAppMenu(s_appTitle, appMenu); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + ::SetAppMenu(s_appTitle, appMenu); + return S_OK; + } } }; diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index b9a95e7b3c..38f8c2a7cb 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -95,6 +95,8 @@ NSMenuItem* AvnAppMenuItem::GetNative() HRESULT AvnAppMenuItem::SetSubMenu (IAvnMenu* menu) { + START_COM_CALL; + @autoreleasepool { if(menu != nullptr) @@ -114,6 +116,8 @@ HRESULT AvnAppMenuItem::SetSubMenu (IAvnMenu* menu) HRESULT AvnAppMenuItem::SetTitle (char* utf8String) { + START_COM_CALL; + @autoreleasepool { if (utf8String != nullptr) @@ -128,6 +132,8 @@ HRESULT AvnAppMenuItem::SetTitle (char* utf8String) HRESULT AvnAppMenuItem::SetGesture (AvnKey key, AvnInputModifiers modifiers) { + START_COM_CALL; + @autoreleasepool { if(key != AvnKeyNone) @@ -183,6 +189,8 @@ HRESULT AvnAppMenuItem::SetGesture (AvnKey key, AvnInputModifiers modifiers) HRESULT AvnAppMenuItem::SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) { + START_COM_CALL; + @autoreleasepool { _predicate = predicate; @@ -193,6 +201,8 @@ HRESULT AvnAppMenuItem::SetAction (IAvnPredicateCallback* predicate, IAvnActionC HRESULT AvnAppMenuItem::SetIsChecked (bool isChecked) { + START_COM_CALL; + @autoreleasepool { [_native setState:(isChecked && _isCheckable ? NSOnState : NSOffState)]; @@ -202,6 +212,8 @@ HRESULT AvnAppMenuItem::SetIsChecked (bool isChecked) HRESULT AvnAppMenuItem::SetToggleType(AvnMenuItemToggleType toggleType) { + START_COM_CALL; + @autoreleasepool { switch(toggleType) @@ -231,6 +243,8 @@ HRESULT AvnAppMenuItem::SetToggleType(AvnMenuItemToggleType toggleType) HRESULT AvnAppMenuItem::SetIcon(void *data, size_t length) { + START_COM_CALL; + @autoreleasepool { if(data != nullptr) @@ -317,6 +331,8 @@ void AvnAppMenu::RaiseClosed() HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item) { + START_COM_CALL; + @autoreleasepool { if([_native hasGlobalMenuItem]) @@ -337,6 +353,8 @@ HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item) HRESULT AvnAppMenu::RemoveItem (IAvnMenuItem* item) { + START_COM_CALL; + @autoreleasepool { auto avnMenuItem = dynamic_cast(item); @@ -352,6 +370,8 @@ HRESULT AvnAppMenu::RemoveItem (IAvnMenuItem* item) HRESULT AvnAppMenu::SetTitle (char* utf8String) { + START_COM_CALL; + @autoreleasepool { if (utf8String != nullptr) @@ -365,6 +385,8 @@ HRESULT AvnAppMenu::SetTitle (char* utf8String) HRESULT AvnAppMenu::Clear() { + START_COM_CALL; + @autoreleasepool { [_native removeAllItems]; diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index e83bf53331..6d5bd4aa02 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -114,6 +114,8 @@ public: virtual HRESULT RunLoop(IAvnLoopCancellation* cancel) override { + START_COM_CALL; + auto can = dynamic_cast(cancel); if(can->Cancelled) return S_OK; diff --git a/native/Avalonia.Native/src/OSX/rendertarget.mm b/native/Avalonia.Native/src/OSX/rendertarget.mm index b2d4341bb9..dc5c24e41e 100644 --- a/native/Avalonia.Native/src/OSX/rendertarget.mm +++ b/native/Avalonia.Native/src/OSX/rendertarget.mm @@ -247,6 +247,8 @@ public: virtual HRESULT GetPixelSize(AvnPixelSize* ret) override { + START_COM_CALL; + if(!_surface) return E_FAIL; *ret = _surface->size; @@ -255,6 +257,8 @@ public: virtual HRESULT GetScaling(double* ret) override { + START_COM_CALL; + if(!_surface) return E_FAIL; *ret = _surface->scale; @@ -281,6 +285,8 @@ public: virtual HRESULT BeginDrawing(IAvnGlSurfaceRenderingSession** ret) override { + START_COM_CALL; + ComPtr releaseContext; @synchronized (_target->lock) { if(_target->surface == nil) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 870345e543..4be1419f78 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -54,6 +54,8 @@ public: virtual HRESULT ObtainNSWindowHandle(void** ret) override { + START_COM_CALL; + if (ret == nullptr) { return E_POINTER; @@ -66,6 +68,8 @@ public: virtual HRESULT ObtainNSWindowHandleRetained(void** ret) override { + START_COM_CALL; + if (ret == nullptr) { return E_POINTER; @@ -78,6 +82,8 @@ public: virtual HRESULT ObtainNSViewHandle(void** ret) override { + START_COM_CALL; + if (ret == nullptr) { return E_POINTER; @@ -90,6 +96,8 @@ public: virtual HRESULT ObtainNSViewHandleRetained(void** ret) override { + START_COM_CALL; + if (ret == nullptr) { return E_POINTER; @@ -107,23 +115,27 @@ public: virtual HRESULT Show(bool activate) override { + START_COM_CALL; + @autoreleasepool { SetPosition(lastPositionSet); UpdateStyle(); [Window setContentView: StandardContainer]; + [Window setTitle:_lastTitle]; if(ShouldTakeFocusOnShow() && activate) { + [Window orderFront: Window]; [Window makeKeyAndOrderFront:Window]; + [Window makeFirstResponder:View]; [NSApp activateIgnoringOtherApps:YES]; } else { [Window orderFront: Window]; } - [Window setTitle:_lastTitle]; _shown = true; @@ -138,6 +150,8 @@ public: virtual HRESULT Hide () override { + START_COM_CALL; + @autoreleasepool { if(Window != nullptr) @@ -152,6 +166,8 @@ public: virtual HRESULT Activate () override { + START_COM_CALL; + @autoreleasepool { if(Window != nullptr) @@ -166,6 +182,8 @@ public: virtual HRESULT SetTopMost (bool value) override { + START_COM_CALL; + @autoreleasepool { [Window setLevel: value ? NSFloatingWindowLevel : NSNormalWindowLevel]; @@ -176,11 +194,16 @@ public: virtual HRESULT Close() override { + START_COM_CALL; + @autoreleasepool { if (Window != nullptr) { - [Window close]; + auto window = Window; + Window = nullptr; + + [window close]; } return S_OK; @@ -189,6 +212,8 @@ public: virtual HRESULT GetClientSize(AvnSize* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -202,8 +227,25 @@ public: } } + virtual HRESULT GetFrameSize(AvnSize* ret) override + { + @autoreleasepool + { + if(ret == nullptr) + return E_POINTER; + + auto frame = [Window frame]; + ret->Width = frame.size.width; + ret->Height = frame.size.height; + + return S_OK; + } + } + virtual HRESULT GetScaling (double* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -222,6 +264,8 @@ public: virtual HRESULT SetMinMaxSize (AvnSize minSize, AvnSize maxSize) override { + START_COM_CALL; + @autoreleasepool { [Window setMinSize: ToNSSize(minSize)]; @@ -233,6 +277,8 @@ public: virtual HRESULT Resize(double x, double y) override { + START_COM_CALL; + @autoreleasepool { auto maxSize = [Window maxSize]; @@ -272,6 +318,8 @@ public: virtual HRESULT Invalidate (AvnRect rect) override { + START_COM_CALL; + @autoreleasepool { [View setNeedsDisplayInRect:[View frame]]; @@ -282,6 +330,8 @@ public: virtual HRESULT SetMainMenu(IAvnMenu* menu) override { + START_COM_CALL; + _mainMenu = menu; auto nativeMenu = dynamic_cast(menu); @@ -300,6 +350,8 @@ public: virtual HRESULT BeginMoveDrag () override { + START_COM_CALL; + @autoreleasepool { auto lastEvent = [View lastMouseDownEvent]; @@ -317,11 +369,15 @@ public: virtual HRESULT BeginResizeDrag (AvnWindowEdge edge) override { + START_COM_CALL; + return S_OK; } virtual HRESULT GetPosition (AvnPoint* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -342,6 +398,8 @@ public: virtual HRESULT SetPosition (AvnPoint point) override { + START_COM_CALL; + @autoreleasepool { lastPositionSet = point; @@ -353,6 +411,8 @@ public: virtual HRESULT PointToClient (AvnPoint point, AvnPoint* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -371,6 +431,8 @@ public: virtual HRESULT PointToScreen (AvnPoint point, AvnPoint* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -388,12 +450,16 @@ public: virtual HRESULT ThreadSafeSetSwRenderedFrame(AvnFramebuffer* fb, IUnknown* dispose) override { + START_COM_CALL; + [View setSwRenderedFrame: fb dispose: dispose]; return S_OK; } virtual HRESULT SetCursor(IAvnCursor* cursor) override { + START_COM_CALL; + @autoreleasepool { Cursor* avnCursor = dynamic_cast(cursor); @@ -423,6 +489,8 @@ public: virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ppv) override { + START_COM_CALL; + if(View == NULL) return E_FAIL; *ppv = [renderTarget createSurfaceRenderTarget]; @@ -431,6 +499,8 @@ public: virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost** retOut) override { + START_COM_CALL; + if(View == NULL) return E_FAIL; *retOut = ::CreateNativeControlHost(View); @@ -439,6 +509,8 @@ public: virtual HRESULT SetBlurEnabled (bool enable) override { + START_COM_CALL; + [StandardContainer ShowBlur:enable]; return S_OK; @@ -448,6 +520,8 @@ public: IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) override { + START_COM_CALL; + auto item = TryGetPasteboardItem(clipboard); [item setString:@"" forType:GetAvnCustomDataType()]; if(item == nil) @@ -513,6 +587,7 @@ private: bool _fullScreenActive; SystemDecorations _decorations; AvnWindowState _lastWindowState; + AvnWindowState _actualWindowState; bool _inSetWindowState; NSRect _preZoomSize; bool _transitioningWindowState; @@ -539,6 +614,7 @@ private: _transitioningWindowState = false; _inSetWindowState = false; _lastWindowState = Normal; + _actualWindowState = Normal; WindowEvents = events; [Window setCanBecomeKeyAndMain]; [Window disableCursorRects]; @@ -547,6 +623,11 @@ private: void HideOrShowTrafficLights () { + if (Window == nil) + { + return; + } + for (id subview in Window.contentView.superview.subviews) { if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) { NSView *titlebarView = [subview subviews][0]; @@ -573,8 +654,10 @@ private: virtual HRESULT Show (bool activate) override { + START_COM_CALL; + @autoreleasepool - { + { WindowBaseImpl::Show(activate); HideOrShowTrafficLights(); @@ -585,6 +668,8 @@ private: virtual HRESULT SetEnabled (bool enable) override { + START_COM_CALL; + @autoreleasepool { [Window setEnabled:enable]; @@ -594,6 +679,8 @@ private: virtual HRESULT SetParent (IAvnWindow* parent) override { + START_COM_CALL; + @autoreleasepool { if(parent == nullptr) @@ -633,7 +720,7 @@ private: void WindowStateChanged () override { - if(!_inSetWindowState && !_transitioningWindowState) + if(_shown && !_inSetWindowState && !_transitioningWindowState) { AvnWindowState state; GetWindowState(&state); @@ -705,6 +792,8 @@ private: virtual HRESULT SetCanResize(bool value) override { + START_COM_CALL; + @autoreleasepool { _canResize = value; @@ -715,6 +804,8 @@ private: virtual HRESULT SetDecorations(SystemDecorations value) override { + START_COM_CALL; + @autoreleasepool { auto currentWindowState = _lastWindowState; @@ -780,6 +871,8 @@ private: virtual HRESULT SetTitle (char* utf8title) override { + START_COM_CALL; + @autoreleasepool { _lastTitle = [NSString stringWithUTF8String:(const char*)utf8title]; @@ -791,6 +884,8 @@ private: virtual HRESULT SetTitleBarColor(AvnColor color) override { + START_COM_CALL; + @autoreleasepool { float a = (float)color.Alpha / 255.0f; @@ -820,6 +915,8 @@ private: virtual HRESULT GetWindowState (AvnWindowState*ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -853,86 +950,111 @@ private: virtual HRESULT TakeFocusFromChildren () override { - if(Window == nil) - return S_OK; - if([Window isKeyWindow]) - [Window makeFirstResponder: View]; + START_COM_CALL; - return S_OK; + @autoreleasepool + { + if(Window == nil) + return S_OK; + if([Window isKeyWindow]) + [Window makeFirstResponder: View]; + + return S_OK; + } } virtual HRESULT SetExtendClientArea (bool enable) override { - _isClientAreaExtended = enable; + START_COM_CALL; - if(enable) + @autoreleasepool { - Window.titleVisibility = NSWindowTitleHidden; + _isClientAreaExtended = enable; - [Window setTitlebarAppearsTransparent:true]; - - auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - - if (wantsTitleBar) - { - [StandardContainer ShowTitleBar:true]; - } - else + if(enable) { - [StandardContainer ShowTitleBar:false]; - } - - if(_extendClientHints & AvnOSXThickTitleBar) - { - Window.toolbar = [NSToolbar new]; - Window.toolbar.showsBaselineSeparator = false; + Window.titleVisibility = NSWindowTitleHidden; + + [Window setTitlebarAppearsTransparent:true]; + + auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + + if (wantsTitleBar) + { + [StandardContainer ShowTitleBar:true]; + } + else + { + [StandardContainer ShowTitleBar:false]; + } + + if(_extendClientHints & AvnOSXThickTitleBar) + { + Window.toolbar = [NSToolbar new]; + Window.toolbar.showsBaselineSeparator = false; + } + else + { + Window.toolbar = nullptr; + } } else { + Window.titleVisibility = NSWindowTitleVisible; Window.toolbar = nullptr; + [Window setTitlebarAppearsTransparent:false]; + View.layer.zPosition = 0; } + + [Window setIsExtended:enable]; + + HideOrShowTrafficLights(); + + UpdateStyle(); + + return S_OK; } - else - { - Window.titleVisibility = NSWindowTitleVisible; - Window.toolbar = nullptr; - [Window setTitlebarAppearsTransparent:false]; - View.layer.zPosition = 0; - } - - [Window setIsExtended:enable]; - - HideOrShowTrafficLights(); - - UpdateStyle(); - - return S_OK; } virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) override { - _extendClientHints = hints; + START_COM_CALL; - SetExtendClientArea(_isClientAreaExtended); - return S_OK; + @autoreleasepool + { + _extendClientHints = hints; + + SetExtendClientArea(_isClientAreaExtended); + return S_OK; + } } virtual HRESULT GetExtendTitleBarHeight (double*ret) override { - if(ret == nullptr) + START_COM_CALL; + + @autoreleasepool { - return E_POINTER; + if(ret == nullptr) + { + return E_POINTER; + } + + *ret = [Window getExtendedTitleBarHeight]; + + return S_OK; } - - *ret = [Window getExtendedTitleBarHeight]; - - return S_OK; } virtual HRESULT SetExtendTitleBarHeight (double value) override { - [StandardContainer SetTitleBarHeightHint:value]; - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + [StandardContainer SetTitleBarHeightHint:value]; + return S_OK; + } } void EnterFullScreenMode () @@ -961,16 +1083,23 @@ private: virtual HRESULT SetWindowState (AvnWindowState state) override { + START_COM_CALL; + @autoreleasepool { - if(_lastWindowState == state) + if(Window == nullptr) + { + return S_OK; + } + + if(_actualWindowState == state) { return S_OK; } _inSetWindowState = true; - auto currentState = _lastWindowState; + auto currentState = _actualWindowState; _lastWindowState = state; if(currentState == Normal) @@ -1049,8 +1178,11 @@ private: } break; } + + _actualWindowState = _lastWindowState; } + _inSetWindowState = false; return S_OK; @@ -1906,7 +2038,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { if(![self windowShouldClose:self]) return; } - + [self close]; } diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 723351ae57..2446c0e1c9 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -17,7 +17,10 @@ namespace ControlCatalog public MainWindow() { this.InitializeComponent(); - this.AttachDevTools(); + this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() + { + StartupScreenIndex = 1, + }); //Renderer.DrawFps = true; //Renderer.DrawDirtyRects = Renderer.DrawFps = true; diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml index e15637aa0f..f0e079ad91 100644 --- a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml +++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml @@ -84,13 +84,13 @@ - - - diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index aa165d13f7..f37df56b73 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -36,6 +36,9 @@ + + + diff --git a/samples/RenderDemo/Pages/CustomAnimatorPage.xaml b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml new file mode 100644 index 0000000000..b386636cae --- /dev/null +++ b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/samples/RenderDemo/Pages/CustomAnimatorPage.xaml.cs b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml.cs new file mode 100644 index 0000000000..eed8ee29ce --- /dev/null +++ b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml.cs @@ -0,0 +1,27 @@ +using System.Reactive.Linq; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using RenderDemo.ViewModels; + +namespace RenderDemo.Pages +{ + public class CustomAnimatorPage : UserControl + { + public CustomAnimatorPage() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/RenderDemo/Pages/CustomStringAnimator.cs b/samples/RenderDemo/Pages/CustomStringAnimator.cs new file mode 100644 index 0000000000..851a2d0187 --- /dev/null +++ b/samples/RenderDemo/Pages/CustomStringAnimator.cs @@ -0,0 +1,16 @@ +using Avalonia.Animation.Animators; + +namespace RenderDemo.Pages +{ + public class CustomStringAnimator : Animator + { + public override string Interpolate(double progress, string oldValue, string newValue) + { + if (newValue.Length == 0) return ""; + var step = 1.0 / newValue.Length; + var length = (int)(progress / step); + var result = newValue.Substring(0, length + 1); + return result; + } + } +} diff --git a/samples/RenderDemo/Pages/TransitionsPage.xaml b/samples/RenderDemo/Pages/TransitionsPage.xaml index f9f69fb341..1985074b0f 100644 --- a/samples/RenderDemo/Pages/TransitionsPage.xaml +++ b/samples/RenderDemo/Pages/TransitionsPage.xaml @@ -141,6 +141,39 @@ + + + + + + + + @@ -166,6 +199,9 @@ + + + diff --git a/samples/Sandbox/Program.cs b/samples/Sandbox/Program.cs index 1e74105196..52321b46a9 100644 --- a/samples/Sandbox/Program.cs +++ b/samples/Sandbox/Program.cs @@ -4,12 +4,12 @@ namespace Sandbox { public class Program { - static void Main(string[] args) - { + static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() - .LogToTrace() - .StartWithClassicDesktopLifetime(args); - } + .LogToTrace(); } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 60b772a183..a72742580c 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -55,6 +55,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform public virtual Size ClientSize => Size.ToSize(RenderScaling); + public Size? FrameSize => null; + public IMouseDevice MouseDevice { get; } = new MouseDevice(); public Action Closed { get; set; } diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index c42153ec4f..172782c5a9 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -3,10 +3,11 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; +using System.Threading; using System.Threading.Tasks; + using Avalonia.Animation.Animators; using Avalonia.Animation.Easings; -using Avalonia.Collections; using Avalonia.Data; using Avalonia.Metadata; @@ -194,6 +195,33 @@ namespace Avalonia.Animation [Content] public KeyFrames Children { get; } = new KeyFrames(); + // Store values for the Animator attached properties for IAnimationSetter objects. + private static readonly Dictionary s_animators = new Dictionary(); + + /// + /// Gets the value of the Animator attached property for a setter. + /// + /// The animation setter. + /// The property animator type. + public static Type GetAnimator(IAnimationSetter setter) + { + if (s_animators.TryGetValue(setter, out var type)) + { + return type; + } + return null; + } + + /// + /// Sets the value of the Animator attached property for a setter. + /// + /// The animation setter. + /// The property animator value. + public static void SetAnimator(IAnimationSetter setter, Type value) + { + s_animators[setter] = value; + } + private readonly static List<(Func Condition, Type Animator)> Animators = new List<(Func, Type)> { ( prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator) ), @@ -248,7 +276,7 @@ namespace Avalonia.Animation { foreach (var setter in keyframe.Setters) { - var handler = GetAnimatorType(setter.Property); + var handler = Animation.GetAnimator(setter) ?? GetAnimatorType(setter.Property); if (handler == null) { @@ -292,7 +320,7 @@ namespace Avalonia.Animation return (newAnimatorInstances, subscriptions); } - /// + /// public IDisposable Apply(Animatable control, IClock clock, IObservable match, Action onComplete) { var (animators, subscriptions) = InterpretKeyframes(control); @@ -317,25 +345,40 @@ namespace Avalonia.Animation if (onComplete != null) { - Task.WhenAll(completionTasks).ContinueWith(_ => onComplete()); + Task.WhenAll(completionTasks).ContinueWith( + (_, state) => ((Action)state).Invoke(), + onComplete); } } return new CompositeDisposable(subscriptions); } - /// - public Task RunAsync(Animatable control, IClock clock = null) + /// + public Task RunAsync(Animatable control, IClock clock = null, CancellationToken cancellationToken = default) { + if (cancellationToken.IsCancellationRequested) + { + return Task.CompletedTask; + } + var run = new TaskCompletionSource(); if (this.IterationCount == IterationCount.Infinite) run.SetException(new InvalidOperationException("Looping animations must not use the Run method.")); - IDisposable subscriptions = null; + IDisposable subscriptions = null, cancellation = null; subscriptions = this.Apply(control, clock, Observable.Return(true), () => { - run.SetResult(null); + run.TrySetResult(null); + subscriptions?.Dispose(); + cancellation?.Dispose(); + }); + + cancellation = cancellationToken.Register(() => + { + run.TrySetResult(null); subscriptions?.Dispose(); + cancellation?.Dispose(); }); return run.Task; diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs index 6f601a3e13..cf79640150 100644 --- a/src/Avalonia.Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Animation/AnimationInstance`1.cs @@ -5,6 +5,7 @@ using Avalonia.Animation.Animators; using Avalonia.Animation.Utils; using Avalonia.Data; using Avalonia.Reactive; +using JetBrains.Annotations; namespace Avalonia.Animation { @@ -36,6 +37,7 @@ namespace Avalonia.Animation private IDisposable _timerSub; private readonly IClock _baseClock; private IClock _clock; + private EventHandler _propertyChangedDelegate; public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action OnComplete, Func Interpolator) { @@ -45,8 +47,6 @@ namespace Avalonia.Animation _onCompleteAction = OnComplete; _interpolator = Interpolator; _baseClock = baseClock; - _neutralValue = (T)_targetControl.GetValue(_animator.Property); - FetchProperties(); } @@ -80,6 +80,7 @@ namespace Avalonia.Animation // Animation may have been stopped before it has finished. ApplyFinalFill(); + _targetControl.PropertyChanged -= _propertyChangedDelegate; _timerSub?.Dispose(); _clock.PlayState = PlayState.Stop; } @@ -88,6 +89,9 @@ namespace Avalonia.Animation { _clock = new Clock(_baseClock); _timerSub = _clock.Subscribe(Step); + _propertyChangedDelegate ??= ControlPropertyChanged; + _targetControl.PropertyChanged += _propertyChangedDelegate; + UpdateNeutralValue(); } public void Step(TimeSpan frameTick) @@ -216,5 +220,22 @@ namespace Avalonia.Animation } } } + + private void UpdateNeutralValue() + { + var property = _animator.Property; + var baseValue = _targetControl.GetBaseValue(property, BindingPriority.LocalValue); + + _neutralValue = baseValue != AvaloniaProperty.UnsetValue ? + (T)baseValue : (T)_targetControl.GetValue(property); + } + + private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _animator.Property && e.Priority > BindingPriority.Animation) + { + UpdateNeutralValue(); + } + } } } diff --git a/src/Avalonia.Animation/ApiCompatBaseline.txt b/src/Avalonia.Animation/ApiCompatBaseline.txt new file mode 100644 index 0000000000..58cb7830e7 --- /dev/null +++ b/src/Avalonia.Animation/ApiCompatBaseline.txt @@ -0,0 +1,6 @@ +Compat issues with assembly Avalonia.Animation: +MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.Animation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock, System.Threading.CancellationToken)' is present in the implementation but not in the contract. +Total Issues: 4 diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs index ff85535d8a..d037834630 100644 --- a/src/Avalonia.Animation/IAnimation.cs +++ b/src/Avalonia.Animation/IAnimation.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; namespace Avalonia.Animation @@ -16,6 +17,6 @@ namespace Avalonia.Animation /// /// Run the animation on the specified control. /// - Task RunAsync(Animatable control, IClock clock); + Task RunAsync(Animatable control, IClock clock, CancellationToken cancellationToken = default); } } diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 173c5c1a94..ae61f8f642 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -585,6 +585,30 @@ namespace Avalonia }); } + /// + /// Subscribes to a property changed notifications for changes that originate from a + /// . + /// + /// The type of the property change sender. + /// /// The type of the property.. + /// The property changed observable. + /// + /// The method to call. The parameters are the sender and the event args. + /// + /// A disposable that can be used to terminate the subscription. + public static IDisposable AddClassHandler( + this IObservable> observable, + Action> action) where TTarget : AvaloniaObject + { + return observable.Subscribe(e => + { + if (e.Sender is TTarget target) + { + action(target, e); + } + }); + } + /// /// Subscribes to a property changed notifications for changes that originate from a /// . diff --git a/src/Avalonia.Base/Metadata/NullableAttributes.cs b/src/Avalonia.Base/Metadata/NullableAttributes.cs index 91f5e81863..b6f0f3a47c 100644 --- a/src/Avalonia.Base/Metadata/NullableAttributes.cs +++ b/src/Avalonia.Base/Metadata/NullableAttributes.cs @@ -1,6 +1,5 @@ #pragma warning disable MA0048 // File name must match type name #define INTERNAL_NULLABLE_ATTRIBUTES -#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 // https://github.com/dotnet/corefx/blob/48363ac826ccf66fbe31a5dcb1dc2aab9a7dd768/src/Common/src/CoreLib/System/Diagnostics/CodeAnalysis/NullableAttributes.cs @@ -10,6 +9,7 @@ namespace System.Diagnostics.CodeAnalysis { +#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 /// Specifies that null is allowed as an input even if the corresponding type disallows it. [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] #if INTERNAL_NULLABLE_ATTRIBUTES @@ -136,5 +136,82 @@ namespace System.Diagnostics.CodeAnalysis /// Gets the condition parameter value. public bool ParameterValue { get; } } -} +#endif // NETSTANDARD2_0 attributes + +#if NETSTANDARD2_1 || NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + /// + /// Specifies that the method or property will ensure that the listed field and property members have + /// not- values. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public #endif + sealed class MemberNotNullAttribute : Attribute + { + /// Gets field or property member names. + public string[] Members { get; } + + /// Initializes the attribute with a field or property member. + /// The field or property member that is promised to be not-null. + public MemberNotNullAttribute(string member) + { + Members = new[] { member }; + } + + /// Initializes the attribute with the list of field and property members. + /// The list of field and property members that are promised to be not-null. + public MemberNotNullAttribute(params string[] members) + { + Members = members; + } + } + + /// + /// Specifies that the method or property will ensure that the listed field and property members have + /// non- values when returning with the specified return value condition. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class MemberNotNullWhenAttribute : Attribute + { + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } + + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, + /// the associated parameter will not be . + /// + /// The field or property member that is promised to be not-. + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = new[] { member }; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// + /// The return value condition. If the method returns this value, + /// the associated parameter will not be . + /// + /// The list of field and property members that are promised to be not-null. + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + } +#endif // NETSTANDARD2_1 attributes +} + diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 1b4632d368..83f13fe199 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -75,7 +75,6 @@ namespace Avalonia.Controls private const double DATAGRID_defaultMinColumnWidth = 20; private const double DATAGRID_defaultMaxColumnWidth = double.PositiveInfinity; - private List _validationErrors; private List _bindingValidationErrors; private IDisposable _validationSubscription; @@ -102,7 +101,6 @@ namespace Avalonia.Controls private bool _areHandlersSuspended; private bool _autoSizingColumns; private IndexToValueTable _collapsedSlotsTable; - private DataGridCellCoordinates _currentCellCoordinates; private Control _clickedElement; // used to store the current column during a Reset @@ -141,7 +139,6 @@ namespace Avalonia.Controls private DataGridSelectedItemsCollection _selectedItems; private bool _temporarilyResetCurrentCell; private object _uneditedValue; // Represents the original current cell value at the time it enters editing mode. - private ICellEditBinding _currentCellEditBinding; // An approximation of the sum of the heights in pixels of the scrolling rows preceding // the first displayed scrolling row. Since the scrolled off rows are discarded, the grid diff --git a/src/Avalonia.Controls.DataGrid/DataGridRows.cs b/src/Avalonia.Controls.DataGrid/DataGridRows.cs index a69b8eafe1..4bfbd7d818 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRows.cs @@ -425,7 +425,7 @@ namespace Avalonia.Controls UpdateDisplayedRows(DisplayData.FirstScrollingSlot, CellsHeight); } - if (DisplayData.FirstScrollingSlot < slot && DisplayData.LastScrollingSlot > slot) + if (DisplayData.FirstScrollingSlot < slot && (DisplayData.LastScrollingSlot > slot || DisplayData.LastScrollingSlot == -1)) { // The row is already displayed in its entirety return true; diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 2f312bb266..1abf4fd6ff 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -4,30 +4,7 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalon InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.IMenuItem.StaysOpenOnClick.set(System.Boolean)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseClosed()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract. -MembersMustExist : Member 'public Avalonia.DirectProperty Avalonia.DirectProperty Avalonia.Controls.NumericUpDown.ValueProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.NumericUpDown.IncrementProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.NumericUpDown.MaximumProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.NumericUpDown.MinimumProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDown.Increment.get()' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDown.Increment.set(System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDown.Maximum.get()' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDown.Maximum.set(System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDown.Minimum.get()' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDown.Minimum.set(System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected System.Double Avalonia.Controls.NumericUpDown.OnCoerceIncrement(System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected System.Double Avalonia.Controls.NumericUpDown.OnCoerceMaximum(System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected System.Double Avalonia.Controls.NumericUpDown.OnCoerceMinimum(System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected System.Double Avalonia.Controls.NumericUpDown.OnCoerceValue(System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.OnIncrementChanged(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.OnMaximumChanged(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.OnMinimumChanged(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.OnValueChanged(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.RaiseValueChangedEvent(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDown.Value.get()' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDown.Value.set(System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDownValueChangedEventArgs..ctor(Avalonia.Interactivity.RoutedEvent, System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.NewValue.get()' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. @@ -35,4 +12,6 @@ EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. -Total Issues: 36 +InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. +Total Issues: 15 diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 54c576bb76..157bebe02b 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -13,6 +13,7 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Threading; +#nullable enable namespace Avalonia { @@ -35,27 +36,27 @@ namespace Avalonia /// /// The application-global data templates. /// - private DataTemplates _dataTemplates; + private DataTemplates? _dataTemplates; private readonly Lazy _clipboard = new Lazy(() => (IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))); private readonly Styler _styler = new Styler(); - private Styles _styles; - private IResourceDictionary _resources; + private Styles? _styles; + private IResourceDictionary? _resources; private bool _notifyingResourcesChanged; - private Action> _stylesAdded; - private Action> _stylesRemoved; + private Action>? _stylesAdded; + private Action>? _stylesRemoved; /// /// Defines the property. /// - public static readonly StyledProperty DataContextProperty = + public static readonly StyledProperty DataContextProperty = StyledElement.DataContextProperty.AddOwner(); /// - public event EventHandler ResourcesChanged; + public event EventHandler? ResourcesChanged; - public event EventHandler UrlsOpened; + public event EventHandler? UrlsOpened; /// /// Creates an instance of the class. @@ -72,7 +73,7 @@ namespace Avalonia /// The data context property specifies the default object that will /// be used for data binding. /// - public object DataContext + public object? DataContext { get { return GetValue(DataContextProperty); } set { SetValue(DataContextProperty, value); } @@ -162,7 +163,7 @@ namespace Avalonia /// /// Gets the styling parent of the application, which is null. /// - IStyleHost IStyleHost.StylingParent => null; + IStyleHost? IStyleHost.StylingParent => null; /// bool IStyleHost.IsStylesInitialized => _styles != null; @@ -194,7 +195,7 @@ namespace Avalonia public virtual void Initialize() { } /// - bool IResourceNode.TryGetResource(object key, out object value) + bool IResourceNode.TryGetResource(object key, out object? value) { value = null; return (_resources?.TryGetResource(key, out value) ?? false) || @@ -279,17 +280,17 @@ namespace Avalonia NotifyResourcesChanged(e); } - private string _name; + private string? _name; /// /// Defines Name property /// - public static readonly DirectProperty NameProperty = - AvaloniaProperty.RegisterDirect("Name", o => o.Name, (o, v) => o.Name = v); + public static readonly DirectProperty NameProperty = + AvaloniaProperty.RegisterDirect("Name", o => o.Name, (o, v) => o.Name = v); /// /// Application name to be used for various platform-specific purposes /// - public string Name + public string? Name { get => _name; set => SetAndRaise(NameProperty, ref _name, value); diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index f8728af87f..89cfb5fa8f 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -77,14 +77,6 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalContentAlignmentProperty = ContentControl.VerticalContentAlignmentProperty.AddOwner(); - /// - /// Defines the property. - /// - public static readonly StyledProperty IsTextSearchEnabledProperty = - AvaloniaProperty.Register(nameof(IsTextSearchEnabled), true); - - private string _textSearchTerm = string.Empty; - private DispatcherTimer _textSearchTimer; private bool _isDropDownOpen; private Popup _popup; private object _selectionBoxItem; @@ -173,15 +165,6 @@ namespace Avalonia.Controls set { SetValue(VerticalContentAlignmentProperty, value); } } - /// - /// Gets or sets a value that specifies whether a user can jump to a value by typing. - /// - public bool IsTextSearchEnabled - { - get { return GetValue(IsTextSearchEnabledProperty); } - set { SetValue(IsTextSearchEnabledProperty, value); } - } - /// protected override IItemContainerGenerator CreateItemContainerGenerator() { @@ -205,7 +188,7 @@ namespace Avalonia.Controls if (e.Handled) return; - if (e.Key == Key.F4 || + if ((e.Key == Key.F4 && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) == false) || ((e.Key == Key.Down || e.Key == Key.Up) && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt))) { IsDropDownOpen = !IsDropDownOpen; @@ -247,32 +230,6 @@ namespace Avalonia.Controls } } - /// - protected override void OnTextInput(TextInputEventArgs e) - { - if (!IsTextSearchEnabled || e.Handled) - return; - - StopTextSearchTimer(); - - _textSearchTerm += e.Text; - - bool match(ItemContainerInfo info) => - info.ContainerControl is IContentControl control && - control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; - - var info = ItemContainerGenerator.Containers.FirstOrDefault(match); - - if (info != null) - { - SelectedIndex = info.Index; - } - - StartTextSearchTimer(); - - e.Handled = true; - } - /// protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { @@ -470,31 +427,5 @@ namespace Avalonia.Controls SelectedIndex = prev; } - - private void StartTextSearchTimer() - { - _textSearchTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; - _textSearchTimer.Tick += TextSearchTimer_Tick; - _textSearchTimer.Start(); - } - - private void StopTextSearchTimer() - { - if (_textSearchTimer == null) - { - return; - } - - _textSearchTimer.Stop(); - _textSearchTimer.Tick -= TextSearchTimer_Tick; - - _textSearchTimer = null; - } - - private void TextSearchTimer_Tick(object sender, EventArgs e) - { - _textSearchTerm = string.Empty; - StopTextSearchTimer(); - } } } diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index 8d893154eb..43bc7d1df9 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -2,6 +2,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Interactivity; using System; using System.Collections.Generic; @@ -88,7 +89,8 @@ namespace Avalonia.Controls /// public static readonly DirectProperty SelectedDateProperty = AvaloniaProperty.RegisterDirect(nameof(SelectedDate), - x => x.SelectedDate, (x, v) => x.SelectedDate = v); + x => x.SelectedDate, (x, v) => x.SelectedDate = v, + defaultBindingMode: BindingMode.TwoWay); // Template Items private Button _flyoutButton; diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index d0cf772c01..6b3f66912f 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs @@ -2,6 +2,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; +using Avalonia.Data; using System; using System.Globalization; @@ -44,7 +45,8 @@ namespace Avalonia.Controls /// public static readonly DirectProperty SelectedTimeProperty = AvaloniaProperty.RegisterDirect(nameof(SelectedTime), - x => x.SelectedTime, (x, v) => x.SelectedTime = v); + x => x.SelectedTime, (x, v) => x.SelectedTime = v, + defaultBindingMode: BindingMode.TwoWay); // Template Items private TimePickerPresenter _presenter; diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index ca0e9d48b8..83470f161d 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -35,6 +35,8 @@ namespace Avalonia.Controls.Embedding.Offscreen } } + public Size? FrameSize => null; + public double RenderScaling { get { return _scaling; } diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index 052b42a233..b9c79e5749 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -1,7 +1,11 @@ +using System.Threading; + using Avalonia.Animation; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; +#nullable enable + namespace Avalonia.Controls { /// @@ -36,8 +40,8 @@ namespace Avalonia.Controls [PseudoClasses(":expanded", ":up", ":down", ":left", ":right")] public class Expander : HeaderedContentControl { - public static readonly StyledProperty ContentTransitionProperty = - AvaloniaProperty.Register(nameof(ContentTransition)); + public static readonly StyledProperty ContentTransitionProperty = + AvaloniaProperty.Register(nameof(ContentTransition)); public static readonly StyledProperty ExpandDirectionProperty = AvaloniaProperty.Register(nameof(ExpandDirection), ExpandDirection.Down); @@ -50,6 +54,7 @@ namespace Avalonia.Controls defaultBindingMode: Data.BindingMode.TwoWay); private bool _isExpanded; + private CancellationTokenSource? _lastTransitionCts; static Expander() { @@ -61,7 +66,7 @@ namespace Avalonia.Controls UpdatePseudoClasses(ExpandDirection); } - public IPageTransition ContentTransition + public IPageTransition? ContentTransition { get => GetValue(ContentTransitionProperty); set => SetValue(ContentTransitionProperty, value); @@ -83,19 +88,23 @@ namespace Avalonia.Controls } } - protected virtual void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e) + protected virtual async void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e) { if (Content != null && ContentTransition != null && Presenter is Visual visualContent) { bool forward = ExpandDirection == ExpandDirection.Left || ExpandDirection == ExpandDirection.Up; + + _lastTransitionCts?.Cancel(); + _lastTransitionCts = new CancellationTokenSource(); + if (IsExpanded) { - ContentTransition.Start(null, visualContent, forward); + await ContentTransition.Start(null, visualContent, forward, _lastTransitionCts.Token); } else { - ContentTransition.Start(visualContent, null, !forward); + await ContentTransition.Start(visualContent, null, forward, _lastTransitionCts.Token); } } } diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs index 0a01767a07..57861163d6 100644 --- a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs +++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs @@ -3,6 +3,7 @@ using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; using System; +using Avalonia.Media.Immutable; namespace Avalonia.Controls { @@ -90,7 +91,7 @@ namespace Avalonia.Controls } else { - _borderRenderHelper.Render(context, Bounds.Size, new Thickness(), CornerRadius, new SolidColorBrush(Material.FallbackColor), null, default); + _borderRenderHelper.Render(context, Bounds.Size, new Thickness(), CornerRadius, new ImmutableSolidColorBrush(Material.FallbackColor), null, default); } } diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 4725d17d65..e4b68c62fd 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -4,6 +4,7 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Layout; using Avalonia.Logging; +using Avalonia.Rendering; #nullable enable @@ -51,8 +52,9 @@ namespace Avalonia.Controls.Primitives private bool _isOpen; private Control? _target; private FlyoutShowMode _showMode = FlyoutShowMode.Standard; - private Rect? enlargedPopupRect; - private IDisposable? transientDisposable; + private Rect? _enlargedPopupRect; + private PixelRect? _enlargePopupRectScreenPixelRect; + private IDisposable? _transientDisposable; protected Popup? Popup { get; private set; } @@ -163,8 +165,10 @@ namespace Avalonia.Controls.Primitives Popup.IsOpen = false; // Ensure this isn't active - transientDisposable?.Dispose(); - transientDisposable = null; + _transientDisposable?.Dispose(); + _transientDisposable = null; + _enlargedPopupRect = null; + _enlargePopupRectScreenPixelRect = null; OnClosed(); } @@ -230,7 +234,7 @@ namespace Avalonia.Controls.Primitives } else if (ShowMode == FlyoutShowMode.TransientWithDismissOnPointerMoveAway) { - transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss); + _transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss); } } @@ -246,20 +250,20 @@ namespace Avalonia.Controls.Primitives // For windowed popups, enlargedPopupRect is in screen coordinates, // for overlay popups, its in OverlayLayer coordinates - if (enlargedPopupRect == null) + if (_enlargedPopupRect == null && _enlargePopupRectScreenPixelRect == null) { // Only do this once when the Flyout opens & cache the result if (Popup?.Host is PopupRoot root) { // Get the popup root bounds and convert to screen coordinates + var tmp = root.Bounds.Inflate(100); - var scPt = root.PointToScreen(tmp.TopLeft); - enlargedPopupRect = new Rect(scPt.X, scPt.Y, tmp.Width, tmp.Height); + _enlargePopupRectScreenPixelRect = new PixelRect(root.PointToScreen(tmp.TopLeft), root.PointToScreen(tmp.BottomRight)); } else if (Popup?.Host is OverlayPopupHost host) { // Overlay popups are in OverlayLayer coordinates, just use that - enlargedPopupRect = host.Bounds.Inflate(100); + _enlargedPopupRect = host.Bounds.Inflate(100); } return; @@ -273,24 +277,18 @@ namespace Avalonia.Controls.Primitives // window will not close this (as pointer events stop), which // does match UWP var pt = pArgs.Root.PointToScreen(pArgs.Position); - if (!enlargedPopupRect?.Contains(new Point(pt.X, pt.Y)) ?? false) + if (!_enlargePopupRectScreenPixelRect?.Contains(pt) ?? false) { HideCore(false); - enlargedPopupRect = null; - transientDisposable?.Dispose(); - transientDisposable = null; } } else if (Popup?.Host is OverlayPopupHost) { // Same as above here, but just different coordinate space // so we don't need to translate - if (!enlargedPopupRect?.Contains(pArgs.Position) ?? false) + if (!_enlargedPopupRect?.Contains(pArgs.Position) ?? false) { HideCore(false); - enlargedPopupRect = null; - transientDisposable?.Dispose(); - transientDisposable = null; } } } diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index c7d598006d..a14df1eb43 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -978,6 +978,7 @@ namespace Avalonia.Controls /// width is not registered in columns. /// Passed through to MeasureCell. /// When "true" cells' desired height is not registered in rows. + /// return true when desired size has changed private void MeasureCellsGroup( int cellsHead, Size referenceSize, diff --git a/src/Avalonia.Controls/GridLength.cs b/src/Avalonia.Controls/GridLength.cs index b8418949d9..ee6a146d61 100644 --- a/src/Avalonia.Controls/GridLength.cs +++ b/src/Avalonia.Controls/GridLength.cs @@ -77,6 +77,12 @@ namespace Avalonia.Controls /// public static GridLength Auto => new GridLength(0, GridUnitType.Auto); + /// + /// Gets an instance of that indicates that a row or column should + /// fill its content. + /// + public static GridLength Star => new GridLength(1, GridUnitType.Star); + /// /// Gets the unit of the . /// diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 20d032f597..55645d4dbb 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Metadata; diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index 4da044fec1..706be376a9 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -17,7 +17,6 @@ namespace Avalonia.Controls private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel { Orientation = Orientation.Horizontal }); - private LightDismissOverlayLayer? _overlay; /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 4c801c2e06..7b06d3c868 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -36,7 +36,7 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty HotKeyProperty = + public static readonly StyledProperty HotKeyProperty = HotKeyManager.HotKeyProperty.AddOwner(); /// @@ -108,7 +108,7 @@ namespace Avalonia.Controls private ICommand? _command; private bool _commandCanExecute = true; private Popup? _popup; - private KeyGesture _hotkey; + private KeyGesture? _hotkey; private bool _isEmbeddedInMenu; /// @@ -214,7 +214,7 @@ namespace Avalonia.Controls /// /// Gets or sets an associated with this control /// - public KeyGesture HotKey + public KeyGesture? HotKey { get { return GetValue(HotKeyProperty); } set { SetValue(HotKeyProperty, value); } diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 09f38042a1..6e53233898 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -22,6 +22,11 @@ namespace Avalonia.Platform /// Size ClientSize { get; } + /// + /// Gets the total size of the toplevel, excluding shadows. + /// + Size? FrameSize { get; } + /// /// Gets the scaling factor for the toplevel. This is used for rendering. /// diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index 7888249bdd..81f43865a7 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -186,7 +186,7 @@ namespace Avalonia.Controls.Presenters if (PageTransition != null && (from != null || to != null)) { - await PageTransition.Start((Visual)from, (Visual)to, fromIndex < toIndex); + await PageTransition.Start((Visual)from, (Visual)to, fromIndex < toIndex, default); } else if (to != null) { diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index b0b52812b9..e3783febdd 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -511,8 +511,8 @@ namespace Avalonia.Controls.Presenters else if (scrollable.IsLogicalScrollEnabled) { Viewport = scrollable.Viewport; - Extent = scrollable.Extent; Offset = scrollable.Offset; + Extent = scrollable.Extent; } } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 3bec46a9ac..ff63e5644f 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -6,6 +6,7 @@ using Avalonia.Metadata; using Avalonia.Threading; using Avalonia.VisualTree; using Avalonia.Layout; +using Avalonia.Media.Immutable; namespace Avalonia.Controls.Presenters { @@ -360,32 +361,31 @@ namespace Avalonia.Controls.Presenters RenderInternal(context); - if (selectionStart == selectionEnd) + if (selectionStart == selectionEnd && _caretBlink) { - var caretBrush = CaretBrush; + var caretBrush = CaretBrush?.ToImmutable(); if (caretBrush is null) { - var backgroundColor = (Background as SolidColorBrush)?.Color; + var backgroundColor = (Background as ISolidColorBrush)?.Color; if (backgroundColor.HasValue) { byte red = (byte)~(backgroundColor.Value.R); byte green = (byte)~(backgroundColor.Value.G); byte blue = (byte)~(backgroundColor.Value.B); - caretBrush = new SolidColorBrush(Color.FromRgb(red, green, blue)); + caretBrush = new ImmutableSolidColorBrush(Color.FromRgb(red, green, blue)); } else + { caretBrush = Brushes.Black; + } } - if (_caretBlink) - { - var (p1, p2) = GetCaretPoints(); - context.DrawLine( - new Pen(caretBrush, 1), - p1, p2); - } + var (p1, p2) = GetCaretPoints(); + context.DrawLine( + new ImmutablePen(caretBrush, 1), + p1, p2); } } diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 9834bf3d3b..a397608aba 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Specialized; using System.Linq; +using System.Resources; using Avalonia.Media; using Avalonia.Rendering; using Avalonia.Utilities; @@ -45,10 +46,27 @@ namespace Avalonia.Controls.Primitives ?.AdornerLayer; } - protected override Size ArrangeOverride(Size finalSize) + protected override Size MeasureOverride(Size availableSize) { - var parent = Parent; + foreach (var child in Children) + { + var info = child.GetValue(s_adornedElementInfoProperty); + + if (info != null && info.Bounds.HasValue) + { + child.Measure(info.Bounds.Value.Bounds.Size); + } + else + { + child.Measure(availableSize); + } + } + return default; + } + + protected override Size ArrangeOverride(Size finalSize) + { foreach (var child in Children) { var info = child.GetValue(s_adornedElementInfoProperty); diff --git a/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs b/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs index 1d655bb691..74d804f2bf 100644 --- a/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs +++ b/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs @@ -52,8 +52,7 @@ namespace Avalonia.Controls.Primitives { if (InputPassThroughElement is object) { - var p = point.Transform(this.TransformToVisual(VisualRoot)!.Value); - var hit = VisualRoot.GetVisualAt(p, x => x != this); + var hit = VisualRoot.GetVisualAt(point, x => x != this); if (hit is object) { diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 34d3347434..2fd08ef77c 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -10,6 +10,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; +using Avalonia.Threading; using Avalonia.VisualTree; #nullable enable @@ -91,6 +92,12 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register( nameof(SelectionMode)); + /// + /// Defines the property. + /// + public static readonly StyledProperty IsTextSearchEnabledProperty = + AvaloniaProperty.Register(nameof(IsTextSearchEnabled), true); + /// /// Event that should be raised by items that implement to /// notify the parent that their selection state @@ -110,6 +117,8 @@ namespace Avalonia.Controls.Primitives RoutingStrategies.Bubble); private static readonly IList Empty = Array.Empty(); + private string _textSearchTerm = string.Empty; + private DispatcherTimer? _textSearchTimer; private ISelectionModel? _selection; private int _oldSelectedIndex; private object? _oldSelectedItem; @@ -305,6 +314,15 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Gets or sets a value that specifies whether a user can jump to a value by typing. + /// + public bool IsTextSearchEnabled + { + get { return GetValue(IsTextSearchEnabledProperty); } + set { SetValue(IsTextSearchEnabledProperty, value); } + } + /// /// Gets or sets the selection mode. /// @@ -490,6 +508,36 @@ namespace Avalonia.Controls.Primitives } } + protected override void OnTextInput(TextInputEventArgs e) + { + if (!e.Handled) + { + if (!IsTextSearchEnabled) + return; + + StopTextSearchTimer(); + + _textSearchTerm += e.Text; + + bool match(ItemContainerInfo info) => + info.ContainerControl is IContentControl control && + control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; + + var info = ItemContainerGenerator.Containers.FirstOrDefault(match); + + if (info != null) + { + SelectedIndex = info.Index; + } + + StartTextSearchTimer(); + + e.Handled = true; + } + + base.OnTextInput(e); + } + protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); @@ -962,6 +1010,32 @@ namespace Avalonia.Controls.Primitives } } + private void StartTextSearchTimer() + { + _textSearchTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; + _textSearchTimer.Tick += TextSearchTimer_Tick; + _textSearchTimer.Start(); + } + + private void StopTextSearchTimer() + { + if (_textSearchTimer == null) + { + return; + } + + _textSearchTimer.Tick -= TextSearchTimer_Tick; + _textSearchTimer.Stop(); + + _textSearchTimer = null; + } + + private void TextSearchTimer_Tick(object sender, EventArgs e) + { + _textSearchTerm = string.Empty; + StopTextSearchTimer(); + } + // When in a BeginInit..EndInit block, or when the DataContext is updating, we need to // defer changes to the selection model because we have no idea in which order properties // will be set. Consider: diff --git a/src/Avalonia.Controls/Remote/RemoteWidget.cs b/src/Avalonia.Controls/Remote/RemoteWidget.cs index fabb38f87d..234960e87c 100644 --- a/src/Avalonia.Controls/Remote/RemoteWidget.cs +++ b/src/Avalonia.Controls/Remote/RemoteWidget.cs @@ -70,7 +70,7 @@ namespace Avalonia.Controls.Remote public override void Render(DrawingContext context) { - if (_lastFrame != null) + if (_lastFrame != null && _lastFrame.Width != 0 && _lastFrame.Height != 0) { var fmt = (PixelFormat) _lastFrame.Format; if (_bitmap == null || _bitmap.PixelSize.Width != _lastFrame.Width || diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index 6e24408aa9..da3c2b15e6 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -27,7 +27,6 @@ namespace Avalonia.Controls private IScrollAnchorProvider _scroller; private IControl _makeAnchorElement; private bool _isAnchorOutsideRealizedRange; - private Task _cacheBuildAction; private Rect _visibleWindow; private Rect _layoutExtent; // This is the expected shift by the layout. diff --git a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs index f8cfde609e..7e6b24f1b5 100644 --- a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs +++ b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs @@ -27,7 +27,6 @@ namespace Avalonia.Controls internal class VirtualizationInfo { private int _pinCounter; - private object _data; public Rect ArrangeBounds { get; set; } public bool AutoRecycleCandidate { get; set; } diff --git a/src/Avalonia.Controls/RowDefinitions.cs b/src/Avalonia.Controls/RowDefinitions.cs index 02ab12b5af..a5ed6ae09d 100644 --- a/src/Avalonia.Controls/RowDefinitions.cs +++ b/src/Avalonia.Controls/RowDefinitions.cs @@ -1,5 +1,4 @@ using System.Linq; -using Avalonia.Collections; namespace Avalonia.Controls { @@ -25,6 +24,11 @@ namespace Avalonia.Controls AddRange(GridLength.ParseLengths(s).Select(x => new RowDefinition(x))); } + public override string ToString() + { + return string.Join(",", this.Select(x => x.Height)); + } + /// /// Parses a string representation of row definitions collection. /// @@ -32,4 +36,4 @@ namespace Avalonia.Controls /// The . public static RowDefinitions Parse(string s) => new RowDefinitions(s); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 90c4e05943..559edeb204 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -176,8 +176,10 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty AllowAutoHideProperty = - ScrollBar.AllowAutoHideProperty.AddOwner(); + public static readonly AttachedProperty AllowAutoHideProperty = + AvaloniaProperty.RegisterAttached( + nameof(AllowAutoHide), + true); /// /// Defines the event. @@ -207,8 +209,8 @@ namespace Avalonia.Controls /// static ScrollViewer() { - HorizontalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); - VerticalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); + HorizontalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); + VerticalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); } /// @@ -526,6 +528,26 @@ namespace Avalonia.Controls return control.GetValue(VerticalScrollBarVisibilityProperty); } + /// + /// Gets the value of the AllowAutoHideProperty attached property. + /// + /// The control to set the value on. + /// The value of the property. + public static void SetAllowAutoHide(Control control, bool value) + { + control.SetValue(AllowAutoHideProperty, value); + } + + /// + /// Gets the value of the AllowAutoHideProperty attached property. + /// + /// The control to read the value from. + /// The value of the property. + public static bool GetAllowAutoHide(Control control) + { + return control.GetValue(AllowAutoHideProperty); + } + /// /// Gets the value of the VerticalScrollBarVisibility attached property. /// @@ -604,10 +626,10 @@ namespace Avalonia.Controls CalculatedPropertiesChanged(); } - private void ScrollBarVisibilityChanged(AvaloniaPropertyChangedEventArgs e) + private void ScrollBarVisibilityChanged(AvaloniaPropertyChangedEventArgs e) { - var wasEnabled = !ScrollBarVisibility.Disabled.Equals(e.OldValue); - var isEnabled = !ScrollBarVisibility.Disabled.Equals(e.NewValue); + var wasEnabled = e.OldValue.GetValueOrDefault() != ScrollBarVisibility.Disabled; + var isEnabled = e.NewValue.GetValueOrDefault() != ScrollBarVisibility.Disabled; if (wasEnabled != isEnabled) { diff --git a/src/Avalonia.Controls/Shapes/Arc.cs b/src/Avalonia.Controls/Shapes/Arc.cs new file mode 100644 index 0000000000..5ebb321f9b --- /dev/null +++ b/src/Avalonia.Controls/Shapes/Arc.cs @@ -0,0 +1,103 @@ +using System; +using Avalonia.Media; + +namespace Avalonia.Controls.Shapes +{ + public class Arc : Shape + { + /// + /// Defines the property. + /// + public static readonly StyledProperty StartAngleProperty = + AvaloniaProperty.Register(nameof(StartAngle), 0.0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty SweepAngleProperty = + AvaloniaProperty.Register(nameof(SweepAngle), 0.0); + + static Arc() + { + StrokeThicknessProperty.OverrideDefaultValue(1); + AffectsGeometry(BoundsProperty, StrokeThicknessProperty, StartAngleProperty, SweepAngleProperty); + } + + /// + /// Gets or sets the angle at which the arc starts, in degrees. + /// + public double StartAngle + { + get => GetValue(StartAngleProperty); + set => SetValue(StartAngleProperty, value); + } + + /// + /// Gets or sets the angle, in degrees, added to the defining where the arc ends. + /// A positive value is clockwise, negative is counter-clockwise. + /// + public double SweepAngle + { + get => GetValue(SweepAngleProperty); + set => SetValue(SweepAngleProperty, value); + } + + protected override Geometry CreateDefiningGeometry() + { + var angle1 = DegreesToRad(StartAngle); + var angle2 = angle1 + DegreesToRad(SweepAngle); + + var startAngle = Math.Min(angle1, angle2); + var sweepAngle = Math.Max(angle1, angle2); + + var normStart = RadToNormRad(startAngle); + var normEnd = RadToNormRad(sweepAngle); + + var rect = new Rect(Bounds.Size); + + if ((normStart == normEnd) && (startAngle != sweepAngle)) // Complete ring. + { + return new EllipseGeometry(rect.Deflate(StrokeThickness / 2)); + } + else if (SweepAngle == 0) + { + return new StreamGeometry(); + } + else // Partial arc. + { + var deflatedRect = rect.Deflate(StrokeThickness / 2); + + var centerX = rect.Center.X; + var centerY = rect.Center.Y; + + var radiusX = deflatedRect.Width / 2; + var radiusY = deflatedRect.Height / 2; + + var angleGap = RadToNormRad(sweepAngle - startAngle); + + var startPoint = GetRingPoint(radiusX, radiusY, centerX, centerY, startAngle); + var endPoint = GetRingPoint(radiusX, radiusY, centerX, centerY, sweepAngle); + + var arcGeometry = new StreamGeometry(); + + using (var ctx = arcGeometry.Open()) + { + ctx.BeginFigure(startPoint, false); + ctx.ArcTo(endPoint, new Size(radiusX, radiusY), angleGap, angleGap >= Math.PI, + SweepDirection.Clockwise); + ctx.EndFigure(false); + } + + return arcGeometry; + } + } + + static double DegreesToRad(double inAngle) => + inAngle * Math.PI / 180; + + static double RadToNormRad(double inAngle) => ((inAngle % (Math.PI * 2)) + (Math.PI * 2)) % (Math.PI * 2); + + static Point GetRingPoint(double radiusX, double radiusY, double centerX, double centerY, double angle) => + new Point((radiusX * Math.Cos(angle)) + centerX, (radiusY * Math.Sin(angle)) + centerY); + } +} diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 0b7595ec9a..0d1d9e3ffe 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Collections; using Avalonia.Media; +using Avalonia.Media.Immutable; #nullable enable @@ -199,8 +200,29 @@ namespace Avalonia.Controls.Shapes if (geometry != null) { - var pen = new Pen(Stroke, StrokeThickness, new DashStyle(StrokeDashArray, StrokeDashOffset), - StrokeLineCap, StrokeJoin); + var stroke = Stroke; + + ImmutablePen? pen = null; + + if (stroke != null) + { + var strokeDashArray = StrokeDashArray; + + ImmutableDashStyle? dashStyle = null; + + if (strokeDashArray != null && strokeDashArray.Count > 0) + { + dashStyle = new ImmutableDashStyle(strokeDashArray, StrokeDashOffset); + } + + pen = new ImmutablePen( + stroke.ToImmutable(), + StrokeThickness, + dashStyle, + StrokeLineCap, + StrokeJoin); + } + context.DrawGeometry(Fill, pen, geometry); } } diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index 4500d52484..0e35c610b2 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -133,7 +133,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(Pane)); /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty PaneTemplateProperty = AvaloniaProperty.Register(nameof(PaneTemplate)); diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 3c221cbf27..ddf55808ce 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -119,9 +119,9 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(RevealPassword)); public static readonly DirectProperty CanCutProperty = - AvaloniaProperty.RegisterDirect( - nameof(CanCut), - o => o.CanCut); + AvaloniaProperty.RegisterDirect( + nameof(CanCut), + o => o.CanCut); public static readonly DirectProperty CanCopyProperty = AvaloniaProperty.RegisterDirect( @@ -129,11 +129,23 @@ namespace Avalonia.Controls o => o.CanCopy); public static readonly DirectProperty CanPasteProperty = - AvaloniaProperty.RegisterDirect( - nameof(CanPaste), - o => o.CanPaste); + AvaloniaProperty.RegisterDirect( + nameof(CanPaste), + o => o.CanPaste); - struct UndoRedoState : IEquatable + public static readonly StyledProperty IsUndoEnabledProperty = + AvaloniaProperty.Register( + nameof(IsUndoEnabled), + defaultValue: true); + + public static readonly DirectProperty UndoLimitProperty = + AvaloniaProperty.RegisterDirect( + nameof(UndoLimit), + o => o.UndoLimit, + (o, v) => o.UndoLimit = v, + unsetValue: -1); + + readonly struct UndoRedoState : IEquatable { public string Text { get; } public int CaretPosition { get; } @@ -218,7 +230,7 @@ namespace Avalonia.Controls value = CoerceCaretIndex(value); SetAndRaise(CaretIndexProperty, ref _caretIndex, value); UndoRedoState state; - if (_undoRedoHelper.TryGetLastState(out state) && state.Text == Text) + if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text) _undoRedoHelper.UpdateLastState(); } } @@ -316,7 +328,7 @@ namespace Avalonia.Controls SelectionEnd = CoerceCaretIndex(SelectionEnd, value); CaretIndex = CoerceCaretIndex(caretIndex, value); - if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing) + if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing) { _undoRedoHelper.Clear(); } @@ -329,7 +341,7 @@ namespace Avalonia.Controls get { return GetSelection(); } set { - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); if (string.IsNullOrEmpty(value)) { DeleteSelection(); @@ -338,7 +350,7 @@ namespace Avalonia.Controls { HandleTextInput(value); } - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); } } @@ -446,6 +458,36 @@ namespace Avalonia.Controls private set { SetAndRaise(CanPasteProperty, ref _canPaste, value); } } + /// + /// Property for determining whether undo/redo is enabled + /// + public bool IsUndoEnabled + { + get { return GetValue(IsUndoEnabledProperty); } + set { SetValue(IsUndoEnabledProperty, value); } + } + + public int UndoLimit + { + get { return _undoRedoHelper.Limit; } + set + { + if (_undoRedoHelper.Limit != value) + { + // can't use SetAndRaise due to using _undoRedoHelper.Limit + // (can't send a ref of a property to SetAndRaise), + // so use RaisePropertyChanged instead. + var oldValue = _undoRedoHelper.Limit; + _undoRedoHelper.Limit = value; + RaisePropertyChanged(UndoLimitProperty, oldValue, value); + } + // from docs at + // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled: + // "Setting UndoLimit clears the undo queue." + _undoRedoHelper.Clear(); + } + } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { _presenter = e.NameScope.Get("PART_TextPresenter"); @@ -465,6 +507,15 @@ namespace Avalonia.Controls UpdatePseudoclasses(); UpdateCommandStates(); } + else if (change.Property == IsUndoEnabledProperty && change.NewValue.GetValueOrDefault() == false) + { + // from docs at + // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled: + // "Setting this property to false clears the undo stack. + // Therefore, if you disable undo and then re-enable it, undo commands still do not work + // because the undo stack was emptied when you disabled undo." + _undoRedoHelper.Clear(); + } } private void UpdateCommandStates() @@ -551,7 +602,10 @@ namespace Avalonia.Controls SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); CaretIndex += input.Length; ClearSelection(); - _undoRedoHelper.DiscardRedo(); + if (IsUndoEnabled) + { + _undoRedoHelper.DiscardRedo(); + } } } @@ -570,10 +624,10 @@ namespace Avalonia.Controls var text = GetSelection(); if (text is null) return; - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); Copy(); DeleteSelection(); - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); } public async void Copy() @@ -591,9 +645,9 @@ namespace Avalonia.Controls if (text is null) return; - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); HandleTextInput(text); - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); } protected override void OnKeyDown(KeyEventArgs e) @@ -638,7 +692,7 @@ namespace Avalonia.Controls Paste(); handled = true; } - else if (Match(keymap.Undo)) + else if (Match(keymap.Undo) && IsUndoEnabled) { try { @@ -652,7 +706,7 @@ namespace Avalonia.Controls handled = true; } - else if (Match(keymap.Redo)) + else if (Match(keymap.Redo) && IsUndoEnabled) { try { @@ -752,7 +806,7 @@ namespace Avalonia.Controls break; case Key.Back: - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); if (hasWholeWordModifiers && SelectionStart == SelectionEnd) { SetSelectionForControlBackspace(); @@ -776,13 +830,13 @@ namespace Avalonia.Controls CaretIndex -= removedCharacters; ClearSelection(); } - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); handled = true; break; case Key.Delete: - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); if (hasWholeWordModifiers && SelectionStart == SelectionEnd) { SetSelectionForControlDelete(); @@ -804,7 +858,7 @@ namespace Avalonia.Controls SetTextInternal(text.Substring(0, caretIndex) + text.Substring(caretIndex + removedCharacters)); } - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); handled = true; break; @@ -812,9 +866,9 @@ namespace Avalonia.Controls case Key.Enter: if (AcceptsReturn) { - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); HandleTextInput(NewLine); - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); handled = true; } @@ -823,9 +877,9 @@ namespace Avalonia.Controls case Key.Tab: if (AcceptsTab) { - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); HandleTextInput("\t"); - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); handled = true; } else @@ -1236,7 +1290,7 @@ namespace Avalonia.Controls private void UpdatePseudoclasses() { - PseudoClasses.Set(":empty", string.IsNullOrWhiteSpace(Text)); + PseudoClasses.Set(":empty", string.IsNullOrEmpty(Text)); } private bool IsPasswordBox => PasswordChar != default(char); @@ -1251,5 +1305,13 @@ namespace Avalonia.Controls ClearSelection(); } } + + private void SnapshotUndoRedo() + { + if (IsUndoEnabled) + { + _undoRedoHelper.Snapshot(); + } + } } } diff --git a/src/Avalonia.Controls/TickBar.cs b/src/Avalonia.Controls/TickBar.cs index 6ea5277a55..237bc2ce1d 100644 --- a/src/Avalonia.Controls/TickBar.cs +++ b/src/Avalonia.Controls/TickBar.cs @@ -1,6 +1,7 @@ using Avalonia.Collections; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Utilities; namespace Avalonia.Controls @@ -295,7 +296,7 @@ namespace Avalonia.Controls endPoint = pt; } - var pen = new Pen(Fill, 1.0d); + var pen = new ImmutablePen(Fill?.ToImmutable(), 1.0d); // Is it Vertical? if (Placement == TickBarPlacement.Left || Placement == TickBarPlacement.Right) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 7a92836ddf..7028dca769 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -42,6 +42,12 @@ namespace Avalonia.Controls public static readonly DirectProperty ClientSizeProperty = AvaloniaProperty.RegisterDirect(nameof(ClientSize), o => o.ClientSize); + /// + /// Defines the property. + /// + public static readonly DirectProperty FrameSizeProperty = + AvaloniaProperty.RegisterDirect(nameof(FrameSize), o => o.FrameSize); + /// /// Defines the property. /// @@ -74,6 +80,7 @@ namespace Avalonia.Controls private readonly IPlatformRenderInterface _renderInterface; private readonly IGlobalStyles _globalStyles; private Size _clientSize; + private Size? _frameSize; private WindowTransparencyLevel _actualTransparencyLevel; private ILayoutManager _layoutManager; private Border _transparencyFallbackBorder; @@ -161,6 +168,7 @@ namespace Avalonia.Controls styler?.ApplyStyles(this); ClientSize = impl.ClientSize; + FrameSize = impl.FrameSize; this.GetObservable(PointerOverElementProperty) .Select( @@ -197,6 +205,15 @@ namespace Avalonia.Controls protected set { SetAndRaise(ClientSizeProperty, ref _clientSize, value); } } + /// + /// Gets or sets the total size of the window. + /// + public Size? FrameSize + { + get { return _frameSize; } + protected set { SetAndRaise(FrameSizeProperty, ref _frameSize, value); } + } + /// /// Gets or sets the that the TopLevel should use when possible. /// @@ -366,6 +383,7 @@ namespace Avalonia.Controls protected virtual void HandleResized(Size clientSize) { ClientSize = clientSize; + FrameSize = PlatformImpl.FrameSize; Width = clientSize.Width; Height = clientSize.Height; LayoutManager.ExecuteLayoutPass(); diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 3afbbd944c..78cd22ae32 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -166,11 +166,9 @@ namespace Avalonia.Controls { item.IsExpanded = true; - var panel = item.Presenter.Panel; - - if (panel != null) + if (item.Presenter?.Panel != null) { - foreach (var child in panel.Children) + foreach (var child in item.Presenter.Panel.Children) { if (child is TreeViewItem treeViewItem) { diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index 3128753781..f8ab58d46e 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; using Avalonia.Utilities; @@ -114,9 +115,9 @@ namespace Avalonia.Controls.Utils var borderThickness = _borderThickness.Top; IPen pen = null; - if (borderThickness > 0) + if (borderBrush != null && borderThickness > 0) { - pen = new Pen(borderBrush, borderThickness); + pen = new ImmutablePen(borderBrush.ToImmutable(), borderThickness); } var rect = new Rect(_size); diff --git a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs index 17cf681f15..7374f20a0c 100644 --- a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs +++ b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs @@ -22,6 +22,10 @@ namespace Avalonia.Controls.Utils private LinkedListNode _currentNode; + /// + /// Maximum number of states this helper can store for undo/redo. + /// If -1, no limit is imposed. + /// public int Limit { get; set; } = 10; public UndoRedoHelper(IUndoRedoHost host) @@ -54,7 +58,10 @@ namespace Avalonia.Controls.Utils public bool HasState => _currentNode != null; public void UpdateLastState(TState state) { - _states.Last.Value = state; + if (_states.Last != null) + { + _states.Last.Value = state; + } } public void UpdateLastState() @@ -86,7 +93,7 @@ namespace Avalonia.Controls.Utils DiscardRedo(); _states.AddLast(current); _currentNode = _states.Last; - if (_states.Count > Limit) + if (Limit != -1 && _states.Count > Limit) _states.RemoveFirst(); } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 700c3d9bad..ae314a33ce 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -592,6 +592,14 @@ namespace Avalonia.Controls owner.RemoveChild(this); } + if (_children.Count > 0) + { + foreach (var child in _children.ToArray()) + { + child.child.Hide(); + } + } + Owner = null; PlatformImpl?.Hide(); @@ -635,6 +643,22 @@ namespace Avalonia.Controls throw new InvalidOperationException("Cannot re-show a closed window."); } + if (parent != null) + { + if (parent.PlatformImpl == null) + { + throw new InvalidOperationException("Cannot show a window with a closed parent."); + } + else if (parent == this) + { + throw new InvalidOperationException("A Window cannot be its own parent."); + } + else if (!parent.IsVisible) + { + throw new InvalidOperationException("Cannot show window with non-visible parent."); + } + } + if (IsVisible) { return; @@ -708,11 +732,22 @@ namespace Avalonia.Controls { throw new ArgumentNullException(nameof(owner)); } - - if (IsVisible) + else if (owner.PlatformImpl == null) + { + throw new InvalidOperationException("Cannot show a window with a closed owner."); + } + else if (owner == this) + { + throw new InvalidOperationException("A Window cannot be its own owner."); + } + else if (IsVisible) { throw new InvalidOperationException("The window is already being shown."); } + else if (!owner.IsVisible) + { + throw new InvalidOperationException("Cannot show window with non-visible parent."); + } RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index cdcb499e98..2b31cef8bd 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -222,6 +222,7 @@ namespace Avalonia.Controls protected override void HandleResized(Size clientSize) { ClientSize = clientSize; + FrameSize = PlatformImpl.FrameSize; LayoutManager.ExecuteLayoutPass(); Renderer?.Resized(clientSize); } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index eedfc52d9d..c8203686f9 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -21,6 +21,7 @@ namespace Avalonia.DesignerSupport.Remote public IPlatformHandle Handle { get; } public Size MaxAutoSizeHint { get; } public Size ClientSize { get; } + public Size? FrameSize => null; public double RenderScaling { get; } = 1.0; public double DesktopScaling => 1.0; public IEnumerable Surfaces { get; } diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index db8684747d..35de491668 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -3,12 +3,16 @@ netstandard2.0 Avalonia Avalonia.Diagnostics + enable %(Filename) + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml index 52720e652f..b7995c38e3 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml @@ -27,7 +27,9 @@ - M7.495 9.052L8.386 11.402H9.477L6.237 3H5.217L2 11.402H3.095L3.933 9.052H7.495ZM5.811 4.453L5.855 4.588L7.173 8.162H4.255L5.562 4.588L5.606 4.453L5.644 4.297L5.676 4.145L5.697 4.019H5.72L5.744 4.145L5.773 4.297L5.811 4.453ZM13.795 10.464V11.4H14.755V7.498C14.755 6.779 14.575 6.226 14.216 5.837C13.857 5.448 13.327 5.254 12.628 5.254C12.429 5.254 12.227 5.273 12.022 5.31C11.817 5.347 11.622 5.394 11.439 5.451C11.256 5.508 11.091 5.569 10.944 5.636C10.797 5.703 10.683 5.765 10.601 5.824V6.808C10.867 6.578 11.167 6.397 11.505 6.268C11.843 6.139 12.194 6.075 12.557 6.075C12.745 6.075 12.915 6.103 13.07 6.16C13.225 6.217 13.357 6.306 13.466 6.427C13.575 6.548 13.659 6.706 13.718 6.899C13.777 7.092 13.806 7.326 13.806 7.599L11.995 7.851C11.651 7.898 11.355 7.977 11.107 8.088C10.859 8.199 10.654 8.339 10.492 8.507C10.33 8.675 10.21 8.868 10.132 9.087C10.054 9.306 10.015 9.546 10.015 9.808C10.015 10.054 10.057 10.283 10.139 10.496C10.221 10.709 10.342 10.893 10.502 11.047C10.662 11.201 10.862 11.323 11.1 11.413C11.338 11.503 11.613 11.548 11.926 11.548C12.328 11.548 12.686 11.456 13.001 11.27C13.316 11.084 13.573 10.816 13.772 10.464H13.795ZM11.667 8.721C11.843 8.657 12.068 8.607 12.341 8.572L13.806 8.367V8.976C13.806 9.222 13.765 9.451 13.683 9.664C13.601 9.877 13.486 10.063 13.34 10.221C13.194 10.379 13.019 10.503 12.816 10.593C12.613 10.683 12.39 10.728 12.148 10.728C11.961 10.728 11.795 10.703 11.653 10.652C11.511 10.601 11.392 10.53 11.296 10.441C11.2 10.352 11.127 10.247 11.076 10.125C11.025 10.003 11 9.873 11 9.732C11 9.568 11.018 9.421 11.055 9.292C11.092 9.163 11.16 9.051 11.257 8.958C11.354 8.865 11.491 8.785 11.667 8.721Z + + M7.495 9.052L8.386 11.402H9.477L6.237 3H5.217L2 11.402H3.095L3.933 9.052H7.495ZM5.811 4.453L5.855 4.588L7.173 8.162H4.255L5.562 4.588L5.606 4.453L5.644 4.297L5.676 4.145L5.697 4.019H5.72L5.744 4.145L5.773 4.297L5.811 4.453ZM13.795 10.464V11.4H14.755V7.498C14.755 6.779 14.575 6.226 14.216 5.837C13.857 5.448 13.327 5.254 12.628 5.254C12.429 5.254 12.227 5.273 12.022 5.31C11.817 5.347 11.622 5.394 11.439 5.451C11.256 5.508 11.091 5.569 10.944 5.636C10.797 5.703 10.683 5.765 10.601 5.824V6.808C10.867 6.578 11.167 6.397 11.505 6.268C11.843 6.139 12.194 6.075 12.557 6.075C12.745 6.075 12.915 6.103 13.07 6.16C13.225 6.217 13.357 6.306 13.466 6.427C13.575 6.548 13.659 6.706 13.718 6.899C13.777 7.092 13.806 7.326 13.806 7.599L11.995 7.851C11.651 7.898 11.355 7.977 11.107 8.088C10.859 8.199 10.654 8.339 10.492 8.507C10.33 8.675 10.21 8.868 10.132 9.087C10.054 9.306 10.015 9.546 10.015 9.808C10.015 10.054 10.057 10.283 10.139 10.496C10.221 10.709 10.342 10.893 10.502 11.047C10.662 11.201 10.862 11.323 11.1 11.413C11.338 11.503 11.613 11.548 11.926 11.548C12.328 11.548 12.686 11.456 13.001 11.27C13.316 11.084 13.573 10.816 13.772 10.464H13.795ZM11.667 8.721C11.843 8.657 12.068 8.607 12.341 8.572L13.806 8.367V8.976C13.806 9.222 13.765 9.451 13.683 9.664C13.601 9.877 13.486 10.063 13.34 10.221C13.194 10.379 13.019 10.503 12.816 10.593C12.613 10.683 12.39 10.728 12.148 10.728C11.961 10.728 11.795 10.703 11.653 10.652C11.511 10.601 11.392 10.53 11.296 10.441C11.2 10.352 11.127 10.247 11.076 10.125C11.025 10.003 11 9.873 11 9.732C11 9.568 11.018 9.421 11.055 9.292C11.092 9.163 11.16 9.051 11.257 8.958C11.354 8.865 11.491 8.785 11.667 8.721Z + @@ -38,7 +40,9 @@ - M1 2H15V3H1V2ZM14 4H13V12H14V4ZM11.272 8.387C11.194 8.088 11.073 7.825 10.912 7.601C10.751 7.377 10.547 7.2 10.303 7.071C10.059 6.942 9.769 6.878 9.437 6.878C9.239 6.878 9.057 6.902 8.89 6.951C8.725 7 8.574 7.068 8.437 7.156C8.301 7.244 8.18 7.35 8.072 7.474L7.893 7.732V4.578H7V12H7.893V11.425L8.019 11.6C8.106 11.702 8.208 11.79 8.323 11.869C8.44 11.947 8.572 12.009 8.721 12.055C8.87 12.101 9.035 12.123 9.219 12.123C9.572 12.123 9.885 12.052 10.156 11.911C10.428 11.768 10.655 11.573 10.838 11.325C11.021 11.075 11.159 10.782 11.252 10.446C11.345 10.108 11.392 9.743 11.392 9.349C11.391 9.007 11.352 8.686 11.272 8.387ZM9.793 7.78C9.944 7.851 10.075 7.956 10.183 8.094C10.292 8.234 10.377 8.407 10.438 8.611C10.489 8.785 10.52 8.982 10.527 9.198L10.52 9.323C10.52 9.65 10.487 9.943 10.42 10.192C10.353 10.438 10.259 10.645 10.142 10.806C10.025 10.968 9.882 11.091 9.721 11.172C9.399 11.334 8.961 11.338 8.652 11.187C8.499 11.112 8.366 11.012 8.259 10.891C8.174 10.795 8.103 10.675 8.041 10.524C8.041 10.524 7.862 10.077 7.862 9.577C7.862 9.077 8.041 8.575 8.041 8.575C8.103 8.398 8.177 8.257 8.265 8.145C8.379 8.002 8.521 7.886 8.689 7.8C8.857 7.714 9.054 7.671 9.276 7.671C9.466 7.671 9.64 7.708 9.793 7.78ZM15 13H1V14H15V13ZM2.813 10L2.085 12.031H1L1.025 11.959L3.466 4.87305H4.407L6.892 12.031H5.81L5.032 10H2.813ZM3.934 6.42205H3.912L3.007 9.17505H4.848L3.934 6.42205Z + + M1 2H15V3H1V2ZM14 4H13V12H14V4ZM11.272 8.387C11.194 8.088 11.073 7.825 10.912 7.601C10.751 7.377 10.547 7.2 10.303 7.071C10.059 6.942 9.769 6.878 9.437 6.878C9.239 6.878 9.057 6.902 8.89 6.951C8.725 7 8.574 7.068 8.437 7.156C8.301 7.244 8.18 7.35 8.072 7.474L7.893 7.732V4.578H7V12H7.893V11.425L8.019 11.6C8.106 11.702 8.208 11.79 8.323 11.869C8.44 11.947 8.572 12.009 8.721 12.055C8.87 12.101 9.035 12.123 9.219 12.123C9.572 12.123 9.885 12.052 10.156 11.911C10.428 11.768 10.655 11.573 10.838 11.325C11.021 11.075 11.159 10.782 11.252 10.446C11.345 10.108 11.392 9.743 11.392 9.349C11.391 9.007 11.352 8.686 11.272 8.387ZM9.793 7.78C9.944 7.851 10.075 7.956 10.183 8.094C10.292 8.234 10.377 8.407 10.438 8.611C10.489 8.785 10.52 8.982 10.527 9.198L10.52 9.323C10.52 9.65 10.487 9.943 10.42 10.192C10.353 10.438 10.259 10.645 10.142 10.806C10.025 10.968 9.882 11.091 9.721 11.172C9.399 11.334 8.961 11.338 8.652 11.187C8.499 11.112 8.366 11.012 8.259 10.891C8.174 10.795 8.103 10.675 8.041 10.524C8.041 10.524 7.862 10.077 7.862 9.577C7.862 9.077 8.041 8.575 8.041 8.575C8.103 8.398 8.177 8.257 8.265 8.145C8.379 8.002 8.521 7.886 8.689 7.8C8.857 7.714 9.054 7.671 9.276 7.671C9.466 7.671 9.64 7.708 9.793 7.78ZM15 13H1V14H15V13ZM2.813 10L2.085 12.031H1L1.025 11.959L3.466 4.87305H4.407L6.892 12.031H5.81L5.032 10H2.813ZM3.934 6.42205H3.912L3.007 9.17505H4.848L3.934 6.42205Z + @@ -49,7 +53,9 @@ - M10.0122 2H10.9879V5.11346L13.5489 3.55609L14.034 4.44095L11.4702 6L14.034 7.55905L13.5489 8.44391L10.9879 6.88654V10H10.0122V6.88654L7.45114 8.44391L6.96606 7.55905L9.5299 6L6.96606 4.44095L7.45114 3.55609L10.0122 5.11346V2ZM2 10H6V14H2V10Z + + M10.0122 2H10.9879V5.11346L13.5489 3.55609L14.034 4.44095L11.4702 6L14.034 7.55905L13.5489 8.44391L10.9879 6.88654V10H10.0122V6.88654L7.45114 8.44391L6.96606 7.55905L9.5299 6L6.96606 4.44095L7.45114 3.55609L10.0122 5.11346V2ZM2 10H6V14H2V10Z + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs index e5b3b080e2..cb98fb70f3 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs @@ -10,8 +10,8 @@ namespace Avalonia.Diagnostics.Controls AvaloniaProperty.RegisterDirect(nameof(Thickness), o => o.Thickness, (o, v) => o.Thickness = v, defaultBindingMode: BindingMode.TwoWay); - public static readonly DirectProperty HeaderProperty = - AvaloniaProperty.RegisterDirect(nameof(Header), o => o.Header, + public static readonly DirectProperty HeaderProperty = + AvaloniaProperty.RegisterDirect(nameof(Header), o => o.Header, (o, v) => o.Header = v); public static readonly DirectProperty IsPresentProperty = @@ -36,7 +36,7 @@ namespace Avalonia.Diagnostics.Controls AvaloniaProperty.Register(nameof(Highlight)); private Thickness _thickness; - private string _header; + private string? _header; private bool _isPresent = true; private double _left; private double _top; @@ -50,7 +50,7 @@ namespace Avalonia.Diagnostics.Controls set => SetAndRaise(ThicknessProperty, ref _thickness, value); } - public string Header + public string? Header { get => _header; set => SetAndRaise(HeaderProperty, ref _header, value); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs b/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs index 63ac3ab62f..0b9044e65e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs @@ -8,12 +8,17 @@ namespace Avalonia.Diagnostics.Converters { public double Opacity { get; set; } - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - return (bool)value ? 1d : Opacity; + if (value is bool boolean && boolean) + { + return 1d; + } + + return Opacity; } - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs b/src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs index 8d10981ba7..4863782f44 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs @@ -7,12 +7,12 @@ namespace Avalonia.Diagnostics.Converters { internal class EnumToCheckedConverter : IValueConverter { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { return Equals(value, parameter); } - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { if (value is bool isChecked && isChecked) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs index 7942d22962..2a386f106e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -6,13 +6,12 @@ using Avalonia.Diagnostics.Views; using Avalonia.Input; using Avalonia.Interactivity; -#nullable enable - namespace Avalonia.Diagnostics { public static class DevTools { - private static readonly Dictionary s_open = new Dictionary(); + private static readonly Dictionary s_open = + new Dictionary(); public static IDisposable Attach(TopLevel root, KeyGesture gesture) { @@ -24,7 +23,7 @@ namespace Avalonia.Diagnostics public static IDisposable Attach(TopLevel root, DevToolsOptions options) { - void PreviewKeyDown(object sender, KeyEventArgs e) + void PreviewKeyDown(object? sender, KeyEventArgs e) { if (options.Gesture.Matches(e)) { @@ -54,6 +53,7 @@ namespace Avalonia.Diagnostics Width = options.Size.Width, Height = options.Size.Height, }; + window.SetOptions(options); window.Closed += DevToolsClosed; s_open.Add(root, window); @@ -71,10 +71,10 @@ namespace Avalonia.Diagnostics return Disposable.Create(() => window?.Close()); } - private static void DevToolsClosed(object sender, EventArgs e) + private static void DevToolsClosed(object? sender, EventArgs e) { - var window = (MainWindow)sender; - s_open.Remove(window.Root); + var window = (MainWindow)sender!; + s_open.Remove(window.Root!); window.Closed -= DevToolsClosed; } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs index f9978f3b7e..5336dca65b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs @@ -22,5 +22,10 @@ namespace Avalonia.Diagnostics /// Gets or sets the initial size of the DevTools window. The default value is 1280x720. /// public Size Size { get; set; } = new Size(1280, 720); + + /// + /// Get or set the startup screen index where the DevTools window will be displayed. + /// + public int? StartupScreenIndex { get; set; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs b/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs index 5927bd785e..4f4579c7d9 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs @@ -22,8 +22,8 @@ The following commands are available: clear(): Clear the output history "; - public dynamic e { get; internal set; } - public dynamic root { get; internal set; } + public dynamic? e { get; internal set; } + public dynamic? root { get; internal set; } internal static object NoOutput { get; } = new object(); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs b/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs index 36fe12d89c..4f493bdcc2 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs @@ -7,9 +7,7 @@ namespace Avalonia.Diagnostics.Models { public EventChainLink(object handler, bool handled, RoutingStrategies route) { - Contract.Requires(handler != null); - - Handler = handler; + Handler = handler ?? throw new ArgumentNullException(nameof(handler)); Handled = handled; Route = route; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs index be3564e781..16852001da 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs @@ -9,12 +9,12 @@ namespace Avalonia.Diagnostics { public IControl Build(object data) { - var name = data.GetType().FullName.Replace("ViewModel", "View"); + var name = data.GetType().FullName!.Replace("ViewModel", "View"); var type = Type.GetType(name); if (type != null) { - return (Control)Activator.CreateInstance(type); + return (Control)Activator.CreateInstance(type)!; } else { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index a9353eba8b..e4c4ca6115 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -1,17 +1,17 @@ -using System.ComponentModel; -using Avalonia.Collections; - namespace Avalonia.Diagnostics.ViewModels { internal class AvaloniaPropertyViewModel : PropertyViewModel { private readonly AvaloniaObject _target; private string _type; - private object _value; + private object? _value; private string _priority; private string _group; +#nullable disable + // Remove "nullable disable" after MemberNotNull will work on our CI. public AvaloniaPropertyViewModel(AvaloniaObject o, AvaloniaProperty property) +#nullable restore { _target = o; Property = property; @@ -20,25 +20,17 @@ namespace Avalonia.Diagnostics.ViewModels $"[{property.OwnerType.Name}.{property.Name}]" : property.Name; - if (property.IsDirect) - { - _group = "Properties"; - Priority = "Direct"; - } - Update(); } public AvaloniaProperty Property { get; } public override object Key => Property; public override string Name { get; } - public bool IsAttached => Property.IsAttached; + public override bool? IsAttached => + Property.IsAttached; - public string Priority - { - get => _priority; - private set => RaiseAndSetIfChanged(ref _priority, value); - } + public override string Priority => + _priority; public override string Type => _type; @@ -56,40 +48,37 @@ namespace Avalonia.Diagnostics.ViewModels } } - public override string Group - { - get => _group; - } + public override string Group => _group; + // [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))] public override void Update() { if (Property.IsDirect) { RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value)); - RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type)); + RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type)); + RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority)); + + _group = "Properties"; } else { var val = _target.GetDiagnostic(Property); RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value)); - RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type)); + RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type)); if (val != null) { - SetGroup(IsAttached ? "Attached Properties" : "Properties"); - Priority = val.Priority.ToString(); + RaiseAndSetIfChanged(ref _priority, val.Priority.ToString(), nameof(Priority)); + RaiseAndSetIfChanged(ref _group, IsAttached == true ? "Attached Properties" : "Properties", nameof(Group)); } else { - SetGroup(Priority = "Unset"); + RaiseAndSetIfChanged(ref _priority, "Unset", nameof(Priority)); + RaiseAndSetIfChanged(ref _group, "Unset", nameof(Group)); } } } - - private void SetGroup(string group) - { - RaiseAndSetIfChanged(ref _group, group, nameof(Group)); - } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index af5e254204..65626aeea5 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -1,5 +1,4 @@ -using System.ComponentModel; -using System.Reflection; +using System.Reflection; namespace Avalonia.Diagnostics.ViewModels { @@ -7,14 +6,17 @@ namespace Avalonia.Diagnostics.ViewModels { private readonly object _target; private string _type; - private object _value; + private object? _value; +#nullable disable + // Remove "nullable disable" after MemberNotNull will work on our CI. public ClrPropertyViewModel(object o, PropertyInfo property) +#nullable restore { _target = o; Property = property; - if (!property.DeclaringType.IsInterface) + if (property.DeclaringType == null || !property.DeclaringType.IsInterface) { Name = property.Name; } @@ -47,11 +49,18 @@ namespace Avalonia.Diagnostics.ViewModels } } + public override string Priority => + string.Empty; + + public override bool? IsAttached => + default; + + // [MemberNotNull(nameof(_type))] public override void Update() { var val = Property.GetValue(_target); RaiseAndSetIfChanged(ref _value, val, nameof(Value)); - RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type)); + RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type)); } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs index 0e0c44ded8..717b49d074 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs @@ -15,11 +15,12 @@ namespace Avalonia.Diagnostics.ViewModels private int _historyIndex = -1; private string _input; private bool _isVisible; - private ScriptState _state; + private ScriptState? _state; public ConsoleViewModel(Action updateContext) { _context = new ConsoleContext(this); + _input = string.Empty; _updateContext = updateContext; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index b1ff8ae98d..3790951b0c 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -18,10 +18,10 @@ namespace Avalonia.Diagnostics.ViewModels { private readonly IVisual _control; private readonly IDictionary> _propertyIndex; - private AvaloniaPropertyViewModel _selectedProperty; + private PropertyViewModel? _selectedProperty; private bool _snapshotStyles; private bool _showInactiveStyles; - private string _styleStatus; + private string? _styleStatus; public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control) { @@ -83,7 +83,8 @@ namespace Avalonia.Diagnostics.ViewModels { foreach (var setter in style.Setters) { - if (setter is Setter regularSetter) + if (setter is Setter regularSetter + && regularSetter.Property != null) { var setterValue = regularSetter.Value; @@ -115,13 +116,14 @@ namespace Avalonia.Diagnostics.ViewModels } } - private (object resourceKey, bool isDynamic)? GetResourceInfo(object value) + private (object resourceKey, bool isDynamic)? GetResourceInfo(object? value) { if (value is StaticResourceExtension staticResource) { return (staticResource.ResourceKey, false); } - else if (value is DynamicResourceExtension dynamicResource) + else if (value is DynamicResourceExtension dynamicResource + && dynamicResource.ResourceKey != null) { return (dynamicResource.ResourceKey, true); } @@ -137,7 +139,7 @@ namespace Avalonia.Diagnostics.ViewModels public ObservableCollection PseudoClasses { get; } - public AvaloniaPropertyViewModel SelectedProperty + public PropertyViewModel? SelectedProperty { get => _selectedProperty; set => RaiseAndSetIfChanged(ref _selectedProperty, value); @@ -155,7 +157,7 @@ namespace Avalonia.Diagnostics.ViewModels set => RaiseAndSetIfChanged(ref _showInactiveStyles, value); } - public string StyleStatus + public string? StyleStatus { get => _styleStatus; set => RaiseAndSetIfChanged(ref _styleStatus, value); @@ -248,7 +250,7 @@ namespace Avalonia.Diagnostics.ViewModels .Select(x => new ClrPropertyViewModel(o, x)); } - private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + private void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { if (_propertyIndex.TryGetValue(e.Property, out var properties)) { @@ -261,9 +263,10 @@ namespace Avalonia.Diagnostics.ViewModels Layout.ControlPropertyChanged(sender, e); } - private void ControlPropertyChanged(object sender, PropertyChangedEventArgs e) + private void ControlPropertyChanged(object? sender, PropertyChangedEventArgs e) { - if (_propertyIndex.TryGetValue(e.PropertyName, out var properties)) + if (e.PropertyName != null + && _propertyIndex.TryGetValue(e.PropertyName, out var properties)) { foreach (var property in properties) { @@ -277,7 +280,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - private void OnClassesChanged(object sender, NotifyCollectionChangedEventArgs e) + private void OnClassesChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (!SnapshotStyles) { @@ -349,10 +352,10 @@ namespace Avalonia.Diagnostics.ViewModels { public static PropertyComparer Instance { get; } = new PropertyComparer(); - public int Compare(PropertyViewModel x, PropertyViewModel y) + public int Compare(PropertyViewModel? x, PropertyViewModel? y) { - var groupX = GroupIndex(x.Group); - var groupY = GroupIndex(y.Group); + var groupX = GroupIndex(x?.Group); + var groupY = GroupIndex(y?.Group); if (groupX != groupY) { @@ -360,11 +363,11 @@ namespace Avalonia.Diagnostics.ViewModels } else { - return string.CompareOrdinal(x.Name, y.Name); + return string.CompareOrdinal(x?.Name, y?.Name); } } - private int GroupIndex(string group) + private int GroupIndex(string? group) { switch (group) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs index b0718bc6ce..4dc0c34c0a 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs @@ -12,14 +12,14 @@ namespace Avalonia.Diagnostics.ViewModels private readonly IVisual _control; private Thickness _borderThickness; private double _height; - private string _heightConstraint; + private string? _heightConstraint; private HorizontalAlignment _horizontalAlignment; private Thickness _marginThickness; private Thickness _paddingThickness; private bool _updatingFromControl; private VerticalAlignment _verticalAlignment; private double _width; - private string _widthConstraint; + private string? _widthConstraint; public ControlLayoutViewModel(IVisual control) { @@ -80,13 +80,13 @@ namespace Avalonia.Diagnostics.ViewModels private set => RaiseAndSetIfChanged(ref _height, value); } - public string WidthConstraint + public string? WidthConstraint { get => _widthConstraint; private set => RaiseAndSetIfChanged(ref _widthConstraint, value); } - public string HeightConstraint + public string? HeightConstraint { get => _heightConstraint; private set => RaiseAndSetIfChanged(ref _heightConstraint, value); @@ -112,7 +112,7 @@ namespace Avalonia.Diagnostics.ViewModels { if (_control is IAvaloniaObject ao) { - string CreateConstraintInfo(StyledProperty minProperty, StyledProperty maxProperty) + string? CreateConstraintInfo(StyledProperty minProperty, StyledProperty maxProperty) { bool hasMin = ao.IsSet(minProperty); bool hasMax = ao.IsSet(maxProperty); @@ -179,7 +179,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - public void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + public void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { try { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs index b56374d353..5b7ddc98ee 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs @@ -27,7 +27,7 @@ namespace Avalonia.Diagnostics.ViewModels if (_updateChildren && value != null) { - foreach (var child in Children) + foreach (var child in Children!) { try { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs index ea54302ebd..65fd81cc78 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs @@ -11,16 +11,13 @@ namespace Avalonia.Diagnostics.ViewModels { private readonly EventsPageViewModel _parentViewModel; private bool _isRegistered; - private FiredEvent _currentEvent; + private FiredEvent? _currentEvent; public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsPageViewModel vm) : base(parent, @event.Name) { - Contract.Requires(@event != null); - Contract.Requires(vm != null); - - Event = @event; - _parentViewModel = vm; + Event = @event ?? throw new ArgumentNullException(nameof(@event)); + _parentViewModel = vm ?? throw new ArgumentNullException(nameof(vm)); } public RoutedEvent Event { get; } @@ -62,18 +59,18 @@ namespace Avalonia.Diagnostics.ViewModels } } - private void HandleEvent(object sender, RoutedEventArgs e) + private void HandleEvent(object? sender, RoutedEventArgs e) { if (!_isRegistered || IsEnabled == false) return; if (sender is IVisual v && BelongsToDevTool(v)) return; - var s = sender; + var s = sender!; var handled = e.Handled; var route = e.Route; - Action handler = delegate + void handler() { if (_currentEvent == null || !_currentEvent.IsPartOfSameEventChain(e)) { @@ -98,14 +95,16 @@ namespace Avalonia.Diagnostics.ViewModels private static bool BelongsToDevTool(IVisual v) { - while (v != null) + var current = v; + + while (current != null) { - if (v is MainView || v is MainWindow) + if (current is MainView || current is MainWindow) { return true; } - v = v.VisualParent; + current = current.VisualParent; } return false; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs index c27cad29e8..e6d7335297 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs @@ -10,14 +10,14 @@ namespace Avalonia.Diagnostics.ViewModels private bool? _isEnabled = false; private bool _isVisible; - protected EventTreeNodeBase(EventTreeNodeBase parent, string text) + protected EventTreeNodeBase(EventTreeNodeBase? parent, string text) { Parent = parent; Text = text; IsVisible = true; } - public IAvaloniaReadOnlyList Children + public IAvaloniaReadOnlyList? Children { get; protected set; @@ -41,7 +41,7 @@ namespace Avalonia.Diagnostics.ViewModels set => RaiseAndSetIfChanged(ref _isVisible, value); } - public EventTreeNodeBase Parent + public EventTreeNodeBase? Parent { get; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs index 7a157dec62..fbcedb2e74 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.ComponentModel; using System.Linq; using Avalonia.Controls; using Avalonia.Diagnostics.Models; @@ -23,8 +22,8 @@ namespace Avalonia.Diagnostics.ViewModels }; private readonly MainViewModel _mainViewModel; - private FiredEvent _selectedEvent; - private EventTreeNodeBase _selectedNode; + private FiredEvent? _selectedEvent; + private EventTreeNodeBase? _selectedNode; public EventsPageViewModel(MainViewModel mainViewModel) { @@ -48,13 +47,13 @@ namespace Avalonia.Diagnostics.ViewModels public ObservableCollection RecordedEvents { get; } = new ObservableCollection(); - public FiredEvent SelectedEvent + public FiredEvent? SelectedEvent { get => _selectedEvent; set => RaiseAndSetIfChanged(ref _selectedEvent, value); } - public EventTreeNodeBase SelectedNode + public EventTreeNodeBase? SelectedNode { get => _selectedNode; set => RaiseAndSetIfChanged(ref _selectedNode, value); @@ -99,7 +98,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - static EventTreeNodeBase FindNode(EventTreeNodeBase node, RoutedEvent eventType) + static EventTreeNodeBase? FindNode(EventTreeNodeBase node, RoutedEvent eventType) { if (node is EventTreeNode eventNode && eventNode.Event == eventType) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs index 0d472a5d8f..5b27236f2e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs @@ -11,10 +11,13 @@ namespace Avalonia.Diagnostics.ViewModels private readonly Dictionary _errors = new Dictionary(); private string _filterString = string.Empty; private bool _useRegexFilter, _useCaseSensitiveFilter, _useWholeWordFilter; - private string _processedFilter; - private Regex _filterRegex; + private Regex? _filterRegex; - public event EventHandler RefreshFilter; + public event EventHandler? RefreshFilter; + + public bool HasErrors => _errors.Count > 0; + + public event EventHandler? ErrorsChanged; public bool Filter(string input) { @@ -31,13 +34,11 @@ namespace Avalonia.Diagnostics.ViewModels } } - _processedFilter = FilterString.Trim(); - try { var options = RegexOptions.Compiled; var pattern = UseRegexFilter - ? _processedFilter : Regex.Escape(_processedFilter); + ? FilterString.Trim() : Regex.Escape(FilterString.Trim()); if (!UseCaseSensitiveFilter) { options |= RegexOptions.IgnoreCase; @@ -109,16 +110,13 @@ namespace Avalonia.Diagnostics.ViewModels } } - public IEnumerable GetErrors(string propertyName) + public IEnumerable GetErrors(string? propertyName) { - if (_errors.TryGetValue(propertyName, out var error)) + if (propertyName != null + && _errors.TryGetValue(propertyName, out var error)) { yield return error; } } - - public bool HasErrors => _errors.Count > 0; - - public event EventHandler ErrorsChanged; } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs index 5fb528eead..32df2f8745 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs @@ -8,15 +8,12 @@ namespace Avalonia.Diagnostics.ViewModels internal class FiredEvent : ViewModelBase { private readonly RoutedEventArgs _eventArgs; - private EventChainLink _handledBy; + private EventChainLink? _handledBy; public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator) { - Contract.Requires(eventArgs != null); - Contract.Requires(originator != null); - - _eventArgs = eventArgs; - Originator = originator; + _eventArgs = eventArgs ?? throw new ArgumentNullException(nameof(eventArgs)); + Originator = originator ?? throw new ArgumentNullException(nameof(originator)); AddToChain(originator); } @@ -25,7 +22,7 @@ namespace Avalonia.Diagnostics.ViewModels return e == _eventArgs; } - public RoutedEvent Event => _eventArgs.RoutedEvent; + public RoutedEvent Event => _eventArgs.RoutedEvent!; public bool IsHandled => HandledBy?.Handled == true; @@ -38,7 +35,7 @@ namespace Avalonia.Diagnostics.ViewModels if (IsHandled) { return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine + - $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}"; + $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy!.HandlerName}"; } return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}"; @@ -47,7 +44,7 @@ namespace Avalonia.Diagnostics.ViewModels public EventChainLink Originator { get; } - public EventChainLink HandledBy + public EventChainLink? HandledBy { get => _handledBy; set diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs index 38788ef8ee..04215fa8ae 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs @@ -7,22 +7,24 @@ namespace Avalonia.Diagnostics.ViewModels { internal class LogicalTreeNode : TreeNode { - public LogicalTreeNode(ILogical logical, TreeNode parent) + public LogicalTreeNode(ILogical logical, TreeNode? parent) : base((Control)logical, parent) { Children = new LogicalTreeNodeCollection(this, logical); } + public override TreeNodeCollection Children { get; } + public static LogicalTreeNode[] Create(object control) { var logical = control as ILogical; - return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null; + return logical != null ? new[] { new LogicalTreeNode(logical, null) } : Array.Empty(); } internal class LogicalTreeNodeCollection : TreeNodeCollection { private readonly ILogical _control; - private IDisposable _subscription; + private IDisposable? _subscription; public LogicalTreeNodeCollection(TreeNode owner, ILogical control) : base(owner) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 49263eafdc..3f367165ac 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -1,10 +1,10 @@ using System; using System.ComponentModel; + using Avalonia.Controls; using Avalonia.Diagnostics.Models; using Avalonia.Input; using Avalonia.Threading; -using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { @@ -17,13 +17,16 @@ namespace Avalonia.Diagnostics.ViewModels private readonly IDisposable _pointerOverSubscription; private ViewModelBase _content; private int _selectedTab; - private string _focusedControl; - private string _pointerOverElement; + private string? _focusedControl; + private string? _pointerOverElement; private bool _shouldVisualizeMarginPadding = true; private bool _shouldVisualizeDirtyRects; private bool _showFpsOverlay; +#nullable disable + // Remove "nullable disable" after MemberNotNull will work on our CI. public MainViewModel(TopLevel root) +#nullable restore { _root = root; _logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root)); @@ -84,6 +87,7 @@ namespace Avalonia.Diagnostics.ViewModels public ViewModelBase Content { get { return _content; } + // [MemberNotNull(nameof(_content))] private set { if (_content is TreePageViewModel oldTree && @@ -114,39 +118,40 @@ namespace Avalonia.Diagnostics.ViewModels public int SelectedTab { get { return _selectedTab; } + // [MemberNotNull(nameof(_content))] set { _selectedTab = value; switch (value) { - case 0: - Content = _logicalTree; - break; case 1: Content = _visualTree; break; case 2: Content = _events; break; + default: + Content = _logicalTree; + break; } RaisePropertyChanged(); } } - public string FocusedControl + public string? FocusedControl { get { return _focusedControl; } private set { RaiseAndSetIfChanged(ref _focusedControl, value); } } - public string PointerOverElement + public string? PointerOverElement { get { return _pointerOverElement; } private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); } } - + private void UpdateConsoleContext(ConsoleContext context) { context.root = _root; @@ -187,7 +192,7 @@ namespace Avalonia.Diagnostics.ViewModels FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name; } - private void KeyboardPropertyChanged(object sender, PropertyChangedEventArgs e) + private void KeyboardPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) { @@ -208,5 +213,12 @@ namespace Avalonia.Diagnostics.ViewModels tree.SelectControl(control); } } + + public int? StartupScreenIndex { get; private set; } = default; + + public void SetOptions(DevToolsOptions options) + { + StartupScreenIndex = options.StartupScreenIndex; + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index e23d6f1471..fdbd8c1aa3 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -16,9 +16,11 @@ namespace Avalonia.Diagnostics.ViewModels public abstract string Group { get; } public abstract string Type { get; } public abstract string Value { get; set; } - public abstract void Update(); + public abstract string Priority { get; } + public abstract bool? IsAttached { get; } + public abstract void Update(); - protected static string ConvertToString(object value) + protected static string ConvertToString(object? value) { if (value is null) { @@ -31,13 +33,13 @@ namespace Avalonia.Diagnostics.ViewModels if (!converter.CanConvertTo(typeof(string)) || converter.GetType() == typeof(CollectionConverter)) { - return value.ToString(); + return value.ToString() ?? "(null)"; } return converter.ConvertToString(value); } - private static object InvokeParse(string s, Type targetType) + private static object? InvokeParse(string s, Type targetType) { var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null); @@ -56,7 +58,7 @@ namespace Avalonia.Diagnostics.ViewModels throw new InvalidCastException("Unable to convert value."); } - protected static object ConvertFromString(string s, Type targetType) + protected static object? ConvertFromString(string s, Type targetType) { var converter = TypeDescriptor.GetConverter(targetType); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs index a82e13fcfa..e93dc7361b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs @@ -8,7 +8,7 @@ namespace Avalonia.Diagnostics.ViewModels public IBrush Tint { get; } - public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object resourceValue, bool isDynamic) : base(property, resourceValue) + public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object? resourceValue, bool isDynamic) : base(property, resourceValue) { Key = resourceKey; Tint = isDynamic ? Brushes.Orange : Brushes.Brown; @@ -16,12 +16,14 @@ namespace Avalonia.Diagnostics.ViewModels public void CopyResourceKey() { - if (Key is null) + var textToCopy = Key?.ToString(); + + if (textToCopy is null) { return; } - CopyToClipboard(Key.ToString()); + CopyToClipboard(textToCopy); } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs index e835f5a878..38cbefcb93 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs @@ -11,7 +11,7 @@ namespace Avalonia.Diagnostics.ViewModels public string Name { get; } - public object Value { get; } + public object? Value { get; } public bool IsActive { @@ -25,7 +25,7 @@ namespace Avalonia.Diagnostics.ViewModels set => RaiseAndSetIfChanged(ref _isVisible, value); } - public SetterViewModel(AvaloniaProperty property, object value) + public SetterViewModel(AvaloniaProperty property, object? value) { Property = property; Name = property.Name; @@ -36,12 +36,14 @@ namespace Avalonia.Diagnostics.ViewModels public void CopyValue() { - if (Value is null) + var textToCopy = Value?.ToString(); + + if (textToCopy is null) { return; } - CopyToClipboard(Value.ToString()); + CopyToClipboard(textToCopy); } public void CopyPropertyName() diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index 9363c28705..4cb470eeac 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -9,17 +9,18 @@ using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { - internal class TreeNode : ViewModelBase, IDisposable + internal abstract class TreeNode : ViewModelBase, IDisposable { - private IDisposable _classesSubscription; + private IDisposable? _classesSubscription; private string _classes; private bool _isExpanded; - public TreeNode(IVisual visual, TreeNode parent) + public TreeNode(IVisual visual, TreeNode? parent) { Parent = parent; Type = visual.GetType().Name; Visual = visual; + _classes = string.Empty; if (visual is IControl control) { @@ -51,10 +52,9 @@ namespace Avalonia.Diagnostics.ViewModels } } - public TreeNodeCollection Children + public abstract TreeNodeCollection Children { get; - protected set; } public string Classes @@ -63,7 +63,7 @@ namespace Avalonia.Diagnostics.ViewModels private set { RaiseAndSetIfChanged(ref _classes, value); } } - public string ElementName + public string? ElementName { get; } @@ -79,7 +79,7 @@ namespace Avalonia.Diagnostics.ViewModels set { RaiseAndSetIfChanged(ref _isExpanded, value); } } - public TreeNode Parent + public TreeNode? Parent { get; } @@ -92,7 +92,7 @@ namespace Avalonia.Diagnostics.ViewModels public void Dispose() { - _classesSubscription.Dispose(); + _classesSubscription?.Dispose(); Children.Dispose(); } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs index 8b4f03bd23..c007411f49 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs @@ -3,46 +3,33 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; + using Avalonia.Collections; namespace Avalonia.Diagnostics.ViewModels { internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList, IDisposable { - private AvaloniaList _inner; + private AvaloniaList? _inner; public TreeNodeCollection(TreeNode owner) => Owner = owner; - public TreeNode this[int index] - { - get - { - EnsureInitialized(); - return _inner[index]; - } - } + public TreeNode this[int index] => EnsureInitialized()[index]; - public int Count - { - get - { - EnsureInitialized(); - return _inner.Count; - } - } + public int Count => EnsureInitialized().Count; protected TreeNode Owner { get; } - public event NotifyCollectionChangedEventHandler CollectionChanged + public event NotifyCollectionChangedEventHandler? CollectionChanged { - add => _inner.CollectionChanged += value; - remove => _inner.CollectionChanged -= value; + add => EnsureInitialized().CollectionChanged += value; + remove => EnsureInitialized().CollectionChanged -= value; } - public event PropertyChangedEventHandler PropertyChanged + public event PropertyChangedEventHandler? PropertyChanged { - add => _inner.PropertyChanged += value; - remove => _inner.PropertyChanged -= value; + add => EnsureInitialized().PropertyChanged += value; + remove => EnsureInitialized().PropertyChanged -= value; } public virtual void Dispose() @@ -58,21 +45,21 @@ namespace Avalonia.Diagnostics.ViewModels public IEnumerator GetEnumerator() { - EnsureInitialized(); - return _inner.GetEnumerator(); + return EnsureInitialized().GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); protected abstract void Initialize(AvaloniaList nodes); - private void EnsureInitialized() + private AvaloniaList EnsureInitialized() { if (_inner is null) { _inner = new AvaloniaList(); Initialize(_inner); } + return _inner; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index 85a7cb69a3..4b18cf414a 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -6,8 +6,8 @@ namespace Avalonia.Diagnostics.ViewModels { internal class TreePageViewModel : ViewModelBase, IDisposable { - private TreeNode _selectedNode; - private ControlDetailsViewModel _details; + private TreeNode? _selectedNode; + private ControlDetailsViewModel? _details; public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes) { @@ -29,7 +29,7 @@ namespace Avalonia.Diagnostics.ViewModels public TreeNode[] Nodes { get; protected set; } - public TreeNode SelectedNode + public TreeNode? SelectedNode { get => _selectedNode; private set @@ -44,7 +44,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - public ControlDetailsViewModel Details + public ControlDetailsViewModel? Details { get => _details; private set @@ -68,7 +68,7 @@ namespace Avalonia.Diagnostics.ViewModels _details?.Dispose(); } - public TreeNode FindNode(IControl control) + public TreeNode? FindNode(IControl control) { foreach (var node in Nodes) { @@ -104,7 +104,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - private void ExpandNode(TreeNode node) + private void ExpandNode(TreeNode? node) { if (node != null) { @@ -113,7 +113,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - private TreeNode FindNode(TreeNode node, IControl control) + private TreeNode? FindNode(TreeNode node, IControl control) { if (node.Visual == control) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs index 66e9c34657..a2ee37c625 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs @@ -1,16 +1,16 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace Avalonia.Diagnostics.ViewModels { internal class ViewModelBase : INotifyPropertyChanged { - private PropertyChangedEventHandler _propertyChanged; + private PropertyChangedEventHandler? _propertyChanged; private List events = new List(); - public event PropertyChangedEventHandler PropertyChanged + public event PropertyChangedEventHandler? PropertyChanged { add { _propertyChanged += value; events.Add("added"); } remove { _propertyChanged -= value; events.Add("removed"); } @@ -20,7 +20,7 @@ namespace Avalonia.Diagnostics.ViewModels { } - protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string propertyName = null) + protected bool RaiseAndSetIfChanged([NotNullIfNotNull("value")] ref T field, T value, [CallerMemberName] string propertyName = null!) { if (!EqualityComparer.Default.Equals(field, value)) { @@ -32,7 +32,7 @@ namespace Avalonia.Diagnostics.ViewModels return false; } - protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) + protected void RaisePropertyChanged([CallerMemberName] string propertyName = null!) { var e = new PropertyChangedEventArgs(propertyName); OnPropertyChanged(e); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index bc40edf477..48fa636664 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -7,7 +7,7 @@ namespace Avalonia.Diagnostics.ViewModels { internal class VisualTreeNode : TreeNode { - public VisualTreeNode(IVisual visual, TreeNode parent) + public VisualTreeNode(IVisual visual, TreeNode? parent) : base(visual, parent) { Children = new VisualTreeNodeCollection(this, visual); @@ -20,16 +20,18 @@ namespace Avalonia.Diagnostics.ViewModels public bool IsInTemplate { get; private set; } + public override TreeNodeCollection Children { get; } + public static VisualTreeNode[] Create(object control) { var visual = control as IVisual; - return visual != null ? new[] { new VisualTreeNode(visual, null) } : null; + return visual != null ? new[] { new VisualTreeNode(visual, null) } : Array.Empty(); } internal class VisualTreeNodeCollection : TreeNodeCollection { private readonly IVisual _control; - private IDisposable _subscription; + private IDisposable? _subscription; public VisualTreeNodeCollection(TreeNode owner, IVisual control) : base(owner) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs index ae70b59fde..ab523fb75a 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs @@ -30,22 +30,26 @@ namespace Avalonia.Diagnostics.Views AvaloniaXamlLoader.Load(this); } - private void HistoryChanged(object sender, NotifyCollectionChangedEventArgs e) + private void HistoryChanged(object? sender, NotifyCollectionChangedEventArgs e) { - if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems[0] is IControl control) + if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems?[0] is IControl control) { DispatcherTimer.RunOnce(control.BringIntoView, TimeSpan.Zero); } } - private void InputKeyDown(object sender, KeyEventArgs e) + private void InputKeyDown(object? sender, KeyEventArgs e) { - var vm = (ConsoleViewModel)DataContext; + var vm = (ConsoleViewModel?)DataContext; + if (vm is null) + { + return; + } switch (e.Key) { case Key.Enter: - vm.Execute(); + _ = vm.Execute(); e.Handled = true; break; case Key.Up: diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs index 687a20c5f6..ba7ab41e35 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs @@ -53,7 +53,7 @@ namespace Avalonia.Diagnostics.Views } } - private void OnRecordedEventsChanged(object sender, NotifyCollectionChangedEventArgs e) + private void OnRecordedEventsChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (sender is ObservableCollection events) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs index 783709e54b..b688ad7676 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs @@ -27,7 +27,11 @@ namespace Avalonia.Diagnostics.Views public void ToggleConsole() { - var vm = (MainViewModel)DataContext; + var vm = (MainViewModel?)DataContext; + if (vm is null) + { + return; + } if (_consoleHeight == -1) { @@ -54,7 +58,7 @@ namespace Avalonia.Diagnostics.Views AvaloniaXamlLoader.Load(this); } - private void PreviewKeyDown(object sender, KeyEventArgs e) + private void PreviewKeyDown(object? sender, KeyEventArgs e) { if (e.Key == Key.Escape) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 330121321a..d1232b749a 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -15,7 +15,7 @@ namespace Avalonia.Diagnostics.Views internal class MainWindow : Window, IStyleHost { private readonly IDisposable _keySubscription; - private TopLevel _root; + private TopLevel? _root; public MainWindow() { @@ -24,9 +24,26 @@ namespace Avalonia.Diagnostics.Views _keySubscription = InputManager.Instance.Process .OfType() .Subscribe(RawKeyDown); + + EventHandler? lh = default; + lh = (s, e) => + { + this.Opened -= lh; + if ((DataContext as MainViewModel)?.StartupScreenIndex is int index) + { + var screens = this.Screens; + if (index > -1 && index < screens.ScreenCount) + { + var screen = screens.All[index]; + this.Position = screen.Bounds.TopLeft; + this.WindowState = WindowState.Maximized; + } + } + }; + this.Opened += lh; } - public TopLevel Root + public TopLevel? Root { get => _root; set @@ -43,7 +60,7 @@ namespace Avalonia.Diagnostics.Views if (_root != null) { _root.Closed += RootClosed; - DataContext = new MainViewModel(value); + DataContext = new MainViewModel(_root); } else { @@ -53,15 +70,20 @@ namespace Avalonia.Diagnostics.Views } } - IStyleHost IStyleHost.StylingParent => null; + IStyleHost? IStyleHost.StylingParent => null; protected override void OnClosed(EventArgs e) { base.OnClosed(e); _keySubscription.Dispose(); - _root.Closed -= RootClosed; - _root = null; - ((MainViewModel)DataContext)?.Dispose(); + + if (_root != null) + { + _root.Closed -= RootClosed; + _root = null; + } + + ((MainViewModel?)DataContext)?.Dispose(); } private void InitializeComponent() @@ -71,12 +93,20 @@ namespace Avalonia.Diagnostics.Views private void RawKeyDown(RawKeyEventArgs e) { + var vm = (MainViewModel?)DataContext; + if (vm is null) + { + return; + } + const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift; if (e.Modifiers == modifiers) { +#pragma warning disable CS0618 // Type or member is obsolete var point = (Root as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default; - +#pragma warning restore CS0618 // Type or member is obsolete + var control = Root.GetVisualsAt(point, x => { if (x is AdornerLayer || !x.IsVisible) return false; @@ -87,7 +117,6 @@ namespace Avalonia.Diagnostics.Views if (control != null) { - var vm = (MainViewModel)DataContext; vm.SelectControl((IControl)control); } } @@ -97,12 +126,14 @@ namespace Avalonia.Diagnostics.Views { var enable = e.Key == Key.S; - var vm = (MainViewModel)DataContext; vm.EnableSnapshotStyles(enable); } } } - private void RootClosed(object sender, EventArgs e) => Close(); + private void RootClosed(object? sender, EventArgs e) => Close(); + + public void SetOptions(DevToolsOptions options) => + (DataContext as MainViewModel)?.SetOptions(options); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs index 3e1a238b36..3543b1adea 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs @@ -14,12 +14,13 @@ namespace Avalonia.Diagnostics.Views internal class TreePageView : UserControl { private readonly Panel _adorner; - private AdornerLayer _currentLayer; + private AdornerLayer? _currentLayer; private TreeView _tree; public TreePageView() { InitializeComponent(); + _tree = this.FindControl("tree"); _tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized; _adorner = new Panel @@ -37,9 +38,15 @@ namespace Avalonia.Diagnostics.Views }; } - protected void AddAdorner(object sender, PointerEventArgs e) + protected void AddAdorner(object? sender, PointerEventArgs e) { - var node = (TreeNode)((Control)sender).DataContext; + var node = (TreeNode?)((Control)sender!).DataContext; + var vm = (TreePageViewModel?)DataContext; + if (node is null || vm is null) + { + return; + } + var visual = (Visual)node.Visual; _currentLayer = AdornerLayer.GetAdornerLayer(visual); @@ -53,8 +60,6 @@ namespace Avalonia.Diagnostics.Views _currentLayer.Children.Add(_adorner); AdornerLayer.SetAdornedElement(_adorner, visual); - var vm = (TreePageViewModel) DataContext; - if (vm.MainView.ShouldVisualizeMarginPadding) { var paddingBorder = (Border)_adorner.Children[0]; @@ -74,7 +79,7 @@ namespace Avalonia.Diagnostics.Views return new Thickness(-input.Left, -input.Top, -input.Right, -input.Bottom); } - protected void RemoveAdorner(object sender, PointerEventArgs e) + protected void RemoveAdorner(object? sender, PointerEventArgs e) { foreach (var border in _adorner.Children.OfType()) { @@ -90,18 +95,17 @@ namespace Avalonia.Diagnostics.Views private void InitializeComponent() { AvaloniaXamlLoader.Load(this); - _tree = this.FindControl("tree"); } - private void TreeViewItemMaterialized(object sender, ItemContainerEventArgs e) + private void TreeViewItemMaterialized(object? sender, ItemContainerEventArgs e) { var item = (TreeViewItem)e.Containers[0].ContainerControl; item.TemplateApplied += TreeViewItemTemplateApplied; } - private void TreeViewItemTemplateApplied(object sender, TemplateAppliedEventArgs e) + private void TreeViewItemTemplateApplied(object? sender, TemplateAppliedEventArgs e) { - var item = (TreeViewItem)sender; + var item = (TreeViewItem)sender!; // This depends on the default tree item template. // We want to handle events in the item header but exclude events coming from children. diff --git a/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs b/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs index 6f699339e7..4adcd32302 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs @@ -17,7 +17,7 @@ namespace Avalonia.Diagnostics private static void PrintVisualTree(IVisual visual, StringBuilder builder, int indent) { - Control control = visual as Control; + Control? control = visual as Control; builder.Append(Indent(indent - 1)); diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index af522f3e36..7f4b9face4 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -41,6 +41,7 @@ namespace Avalonia.Headless } public Size ClientSize { get; set; } + public Size? FrameSize => null; public double RenderScaling { get; } = 1; public double DesktopScaling => RenderScaling; public IEnumerable Surfaces { get; } diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 79152e20d2..a159b19026 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -217,12 +217,31 @@ namespace Avalonia.Input { var bindings = (currentHandler as IInputElement)?.KeyBindings; if (bindings != null) + { + KeyBinding[]? bindingsCopy = null; + + // Create a copy of the KeyBindings list if there's a binding which matches the event. + // If we don't do this the foreach loop will throw an InvalidOperationException when the KeyBindings list is changed. + // This can happen when a new view is loaded which adds its own KeyBindings to the handler. foreach (var binding in bindings) { - if (ev.Handled) + if (binding.Gesture?.Matches(ev) == true) + { + bindingsCopy = bindings.ToArray(); break; - binding.TryHandle(ev); + } + } + + if (bindingsCopy is object) + { + foreach (var binding in bindingsCopy) + { + if (ev.Handled) + break; + binding.TryHandle(ev); + } } + } currentHandler = currentHandler.VisualParent; } diff --git a/src/Avalonia.Layout/ElementManager.cs b/src/Avalonia.Layout/ElementManager.cs index cb13deb15f..3f106708e6 100644 --- a/src/Avalonia.Layout/ElementManager.cs +++ b/src/Avalonia.Layout/ElementManager.cs @@ -325,7 +325,10 @@ namespace Avalonia.Layout break; case NotifyCollectionChangedAction.Move: - throw new NotImplementedException(); + int size = args.OldItems != null ? args.OldItems.Count : 1; + OnItemsRemoved(args.OldStartingIndex, size); + OnItemsAdded(args.NewStartingIndex, size); + break; } } } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index f716464d14..ced9cea3a8 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -104,6 +104,20 @@ namespace Avalonia.Native } } + public Size? FrameSize + { + get + { + if (_native != null) + { + var s = _native.FrameSize; + return new Size(s.Width, s.Height); + } + + return default; + } + } + public IEnumerable Surfaces => new[] { (_gpu ? _glSurface : (object)null), this diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index adcbeb2d3a..89e20463d8 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -435,6 +435,7 @@ interface IAvnWindowBase : IUnknown HRESULT Close(); HRESULT Activate(); HRESULT GetClientSize(AvnSize*ret); + HRESULT GetFrameSize(AvnSize*ret); HRESULT GetScaling(double*ret); HRESULT SetMinMaxSize(AvnSize minSize, AvnSize maxSize); HRESULT Resize(double width, double height); diff --git a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs index 9685ecbe91..c4dd79f468 100644 --- a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs +++ b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; + using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Styling; @@ -22,7 +24,9 @@ namespace Avalonia.ReactiveUI /// public static readonly StyledProperty DefaultContentProperty = AvaloniaProperty.Register(nameof(DefaultContent)); - + + private CancellationTokenSource? _lastTransitionCts; + /// /// Gets or sets the animation played when content appears and disappears. /// @@ -62,11 +66,14 @@ namespace Avalonia.ReactiveUI /// New content to set. private async void UpdateContentWithTransition(object? content) { + _lastTransitionCts?.Cancel(); + _lastTransitionCts = new CancellationTokenSource(); + if (PageTransition != null) - await PageTransition.Start(this, null, true); + await PageTransition.Start(this, null, true, _lastTransitionCts.Token); base.Content = content; if (PageTransition != null) - await PageTransition.Start(null, this, true); + await PageTransition.Start(null, this, true, _lastTransitionCts.Token); } } } diff --git a/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs b/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs index 9e30e4fa14..8d446ebc9c 100644 --- a/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs +++ b/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs @@ -33,6 +33,6 @@ namespace Avalonia.Styling.Activators void IObserver.OnCompleted() { } void IObserver.OnError(Exception error) { } - void IObserver.OnNext(object value) => PublishNext(Equals(value, _value)); + void IObserver.OnNext(object value) => PublishNext(PropertyEqualsSelector.Compare(_property.PropertyType, value, _value)); } } diff --git a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs index cdd985ac80..5d9c3fe56b 100644 --- a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs @@ -1,4 +1,6 @@ using System; +using System.ComponentModel; +using System.Globalization; using System.Text; using Avalonia.Styling.Activators; @@ -75,11 +77,37 @@ namespace Avalonia.Styling } else { - var result = (control.GetValue(_property) ?? string.Empty).Equals(_value); - return result ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; + return Compare(_property.PropertyType, control.GetValue(_property), _value) + ? SelectorMatch.AlwaysThisInstance + : SelectorMatch.NeverThisInstance; } + } protected override Selector? MovePrevious() => _previous; + + internal static bool Compare(Type propertyType, object propertyValue, object? value) + { + if (propertyType == typeof(object) && + propertyValue?.GetType() is Type inferredType) + { + propertyType = inferredType; + } + + var valueType = value?.GetType(); + + if (valueType is null || propertyType.IsAssignableFrom(valueType)) + { + return Equals(propertyValue, value); + } + + var converter = TypeDescriptor.GetConverter(propertyType); + if (converter?.CanConvertFrom(valueType) == true) + { + return Equals(propertyValue, converter.ConvertFrom(null, CultureInfo.InvariantCulture, value)); + } + + return false; + } } } diff --git a/src/Avalonia.Themes.Default/ListBox.xaml b/src/Avalonia.Themes.Default/ListBox.xaml index e91d8a6772..1ad996a1a0 100644 --- a/src/Avalonia.Themes.Default/ListBox.xaml +++ b/src/Avalonia.Themes.Default/ListBox.xaml @@ -13,7 +13,8 @@ + VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" + AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"> + VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" + AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"> + VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" + AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"> + VerticalAlignment="Center" /> + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml index ae4fdca490..9e8db19feb 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml @@ -24,7 +24,8 @@ + VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" + AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"> + VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" + AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"> + VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" + AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"> PageTransitions { get; set; } = new List(); - /// - /// Starts the animation. - /// - /// - /// The control that is being transitioned away from. May be null. - /// - /// - /// The control that is being transitioned to. May be null. - /// - /// - /// Defines the direction of the transition. - /// - /// - /// A that tracks the progress of the animation. - /// - public Task Start(Visual from, Visual to, bool forward) + /// + public Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken) { var transitionTasks = PageTransitions - .Select(transition => transition.Start(from, to, forward)) + .Select(transition => transition.Start(from, to, forward, cancellationToken)) .ToList(); return Task.WhenAll(transitionTasks); } diff --git a/src/Avalonia.Visuals/Animation/CrossFade.cs b/src/Avalonia.Visuals/Animation/CrossFade.cs index 0615b854da..5eaa920b32 100644 --- a/src/Avalonia.Visuals/Animation/CrossFade.cs +++ b/src/Avalonia.Visuals/Animation/CrossFade.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Threading; using System.Threading.Tasks; using Avalonia.Animation.Easings; using Avalonia.Styling; @@ -97,49 +99,39 @@ namespace Avalonia.Animation set => _fadeOutAnimation.Easing = value; } - /// - /// Starts the animation. - /// - /// - /// The control that is being transitioned away from. May be null. - /// - /// - /// The control that is being transitioned to. May be null. - /// - /// - /// A that tracks the progress of the animation. - /// - public async Task Start(Visual from, Visual to) + /// + public async Task Start(Visual from, Visual to, CancellationToken cancellationToken) { - var tasks = new List(); - - if (to != null) - { - to.Opacity = 0; - } - - if (from != null) + if (cancellationToken.IsCancellationRequested) { - tasks.Add(_fadeOutAnimation.RunAsync(from)); + return; } - if (to != null) + var tasks = new List(); + using (var disposables = new CompositeDisposable()) { - to.IsVisible = true; - tasks.Add(_fadeInAnimation.RunAsync(to)); + if (to != null) + { + disposables.Add(to.SetValue(Visual.OpacityProperty, 0, Data.BindingPriority.Animation)); + } - } + if (from != null) + { + tasks.Add(_fadeOutAnimation.RunAsync(from, null, cancellationToken)); + } - await Task.WhenAll(tasks); + if (to != null) + { + to.IsVisible = true; + tasks.Add(_fadeInAnimation.RunAsync(to, null, cancellationToken)); + } - if (from != null) - { - from.IsVisible = false; - } + await Task.WhenAll(tasks); - if (to != null) - { - to.Opacity = 1; + if (from != null && !cancellationToken.IsCancellationRequested) + { + from.IsVisible = false; + } } } @@ -155,12 +147,13 @@ namespace Avalonia.Animation /// /// Unused for cross-fades. /// + /// allowed cancel transition /// /// A that tracks the progress of the animation. /// - Task IPageTransition.Start(Visual from, Visual to, bool forward) + Task IPageTransition.Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken) { - return Start(from, to); + return Start(from, to, cancellationToken); } } } diff --git a/src/Avalonia.Visuals/Animation/IPageTransition.cs b/src/Avalonia.Visuals/Animation/IPageTransition.cs index 659bc12424..2d19ddbb5b 100644 --- a/src/Avalonia.Visuals/Animation/IPageTransition.cs +++ b/src/Avalonia.Visuals/Animation/IPageTransition.cs @@ -1,3 +1,4 @@ +using System.Threading; using System.Threading.Tasks; namespace Avalonia.Animation @@ -19,9 +20,12 @@ namespace Avalonia.Animation /// /// If the animation is bidirectional, controls the direction of the animation. /// + /// + /// Animation cancellation. + /// /// /// A that tracks the progress of the animation. /// - Task Start(Visual from, Visual to, bool forward); + Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken); } } diff --git a/src/Avalonia.Visuals/Animation/PageSlide.cs b/src/Avalonia.Visuals/Animation/PageSlide.cs index dd5d598e12..7d033ccf61 100644 --- a/src/Avalonia.Visuals/Animation/PageSlide.cs +++ b/src/Avalonia.Visuals/Animation/PageSlide.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Avalonia.Animation.Easings; using Avalonia.Media; @@ -60,23 +61,14 @@ namespace Avalonia.Animation /// public Easing SlideOutEasing { get; set; } = new LinearEasing(); - /// - /// Starts the animation. - /// - /// - /// The control that is being transitioned away from. May be null. - /// - /// - /// The control that is being transitioned to. May be null. - /// - /// - /// If true, the new page is slid in from the right, or if false from the left. - /// - /// - /// A that tracks the progress of the animation. - /// - public async Task Start(Visual from, Visual to, bool forward) + /// + public async Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken) { + if (cancellationToken.IsCancellationRequested) + { + return; + } + var tasks = new List(); var parent = GetVisualParent(from, to); var distance = Orientation == SlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height; @@ -109,7 +101,7 @@ namespace Avalonia.Animation }, Duration = Duration }; - tasks.Add(animation.RunAsync(from)); + tasks.Add(animation.RunAsync(from, null, cancellationToken)); } if (to != null) @@ -140,12 +132,12 @@ namespace Avalonia.Animation }, Duration = Duration }; - tasks.Add(animation.RunAsync(to)); + tasks.Add(animation.RunAsync(to, null, cancellationToken)); } await Task.WhenAll(tasks); - if (from != null) + if (from != null && !cancellationToken.IsCancellationRequested) { from.IsVisible = false; } diff --git a/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs new file mode 100644 index 0000000000..cc5af1b4b1 --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs @@ -0,0 +1,59 @@ +using System; +using Avalonia.Animation.Animators; +using Avalonia.Animation.Easings; +using Avalonia.Media; + +#nullable enable + +namespace Avalonia.Animation +{ + /// + /// Transition class that handles with type. + /// Only values of will transition correctly at the moment. + /// + public class BrushTransition : Transition + { + private static readonly ISolidColorBrushAnimator s_animator = new ISolidColorBrushAnimator(); + + public override IObservable DoTransition(IObservable progress, IBrush? oldValue, IBrush? newValue) + { + var oldSolidColorBrush = TryGetSolidColorBrush(oldValue); + var newSolidColorBrush = TryGetSolidColorBrush(newValue); + + if (oldSolidColorBrush != null && newSolidColorBrush != null) + { + return new AnimatorTransitionObservable( + s_animator, progress, Easing, oldSolidColorBrush, newSolidColorBrush); + } + + return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue); + } + + private static ISolidColorBrush? TryGetSolidColorBrush(IBrush? brush) + { + if (brush is null) + { + return Brushes.Transparent; + } + + return brush as ISolidColorBrush; + } + + private class IncompatibleTransitionObservable : TransitionObservableBase + { + private readonly IBrush? _from; + private readonly IBrush? _to; + + public IncompatibleTransitionObservable(IObservable progress, Easing easing, IBrush? from, IBrush? to) : base(progress, easing) + { + _from = from; + _to = to; + } + + protected override IBrush? ProduceValue(double progress) + { + return progress < 0.5 ? _from : _to; + } + } + } +} diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index f9fd125615..39a4c3004c 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -1,4 +1,11 @@ Compat issues with assembly Avalonia.Visuals: +MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.CompositePageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.CrossFade.Start(Avalonia.Visual, Avalonia.Visual)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean, System.Threading.CancellationToken)' is present in the implementation but not in the contract. +MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.PageSlide.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. +TypeCannotChangeClassification : Type 'Avalonia.Media.Immutable.ImmutableSolidColorBrush' is a 'class' in the implementation but is a 'struct' in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract. CannotSealType : Type 'Avalonia.Media.TextFormatting.GenericTextParagraphProperties' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. @@ -63,9 +70,8 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalon InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' is present in the contract but not in the implementation. MembersMustExist : Member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' does not exist in the implementation but it does exist in the contract. -Total Issues: 64 InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.IO.Stream)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.String)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToHeight(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToWidth(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. -Total Issues: 11 +Total Issues: 75 diff --git a/src/Avalonia.Visuals/Media/BoxShadow.cs b/src/Avalonia.Visuals/Media/BoxShadow.cs index 69395fd3b8..50f75365b0 100644 --- a/src/Avalonia.Visuals/Media/BoxShadow.cs +++ b/src/Avalonia.Visuals/Media/BoxShadow.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Text; using Avalonia.Animation.Animators; using Avalonia.Utilities; @@ -75,6 +76,46 @@ namespace Avalonia.Media return rv; } } + + public override string ToString() + { + var sb = new StringBuilder(); + + if (IsEmpty) + { + return "none"; + } + + if (IsInset) + { + sb.Append("inset"); + } + + if (OffsetX != 0.0) + { + sb.AppendFormat(" {0}", OffsetX.ToString()); + } + + if (OffsetY != 0.0) + { + sb.AppendFormat(" {0}", OffsetY.ToString()); + } + + if (Blur != 0.0) + { + sb.AppendFormat(" {0}", Blur.ToString()); + } + + if (Spread != 0.0) + { + sb.AppendFormat(" {0}", Spread.ToString()); + } + + sb.AppendFormat(" {0}", Color.ToString()); + + return sb.ToString(); + } + public static unsafe BoxShadow Parse(string s) { if(s == null) diff --git a/src/Avalonia.Visuals/Media/BoxShadows.cs b/src/Avalonia.Visuals/Media/BoxShadows.cs index 9e4d6aacb0..810ac70b99 100644 --- a/src/Avalonia.Visuals/Media/BoxShadows.cs +++ b/src/Avalonia.Visuals/Media/BoxShadows.cs @@ -1,6 +1,6 @@ using System; -using System.Collections.Generic; using System.ComponentModel; +using System.Text; using Avalonia.Animation.Animators; namespace Avalonia.Media @@ -43,6 +43,24 @@ namespace Avalonia.Media } } + public override string ToString() + { + var sb = new StringBuilder(); + + if (Count == 0) + { + return "none"; + } + + foreach (var boxShadow in this) + { + sb.AppendFormat("{0} ", boxShadow.ToString()); + } + + return sb.ToString(); + + } + [EditorBrowsable(EditorBrowsableState.Never)] public struct BoxShadowsEnumerator { diff --git a/src/Avalonia.Visuals/Media/Brush.cs b/src/Avalonia.Visuals/Media/Brush.cs index fb03d19a4e..cf7f5f531c 100644 --- a/src/Avalonia.Visuals/Media/Brush.cs +++ b/src/Avalonia.Visuals/Media/Brush.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel; using Avalonia.Animation; using Avalonia.Animation.Animators; +using Avalonia.Media.Immutable; namespace Avalonia.Media { @@ -47,7 +48,7 @@ namespace Avalonia.Media if (s[0] == '#') { - return new SolidColorBrush(Color.Parse(s)); + return new ImmutableSolidColorBrush(Color.Parse(s)); } var brush = KnownColors.GetKnownBrush(s); diff --git a/src/Avalonia.Visuals/Media/ImageDrawing.cs b/src/Avalonia.Visuals/Media/ImageDrawing.cs new file mode 100644 index 0000000000..82f97b52b4 --- /dev/null +++ b/src/Avalonia.Visuals/Media/ImageDrawing.cs @@ -0,0 +1,53 @@ +#nullable enable + +namespace Avalonia.Media +{ + /// + /// Draws an image within a region defined by a . + /// + public class ImageDrawing : Drawing + { + /// + /// Defines the property. + /// + public static readonly StyledProperty ImageSourceProperty = + AvaloniaProperty.Register(nameof(ImageSource)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty RectProperty = + AvaloniaProperty.Register(nameof(Rect)); + + /// + /// Gets or sets the source of the image. + /// + public IImage? ImageSource + { + get => GetValue(ImageSourceProperty); + set => SetValue(ImageSourceProperty, value); + } + + /// + /// Gets or sets region in which the image is drawn. + /// + public Rect Rect + { + get => GetValue(RectProperty); + set => SetValue(RectProperty, value); + } + + public override void Draw(DrawingContext context) + { + var imageSource = ImageSource; + var rect = Rect; + + if (imageSource is object && !rect.IsEmpty) + { + context.DrawImage(imageSource, rect); + } + } + + public override Rect GetBounds() => Rect; + } +} diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs index e9a52fe6ed..2dd188e0a9 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs @@ -10,6 +10,8 @@ namespace Avalonia.Media.Immutable /// public class ImmutableDashStyle : IDashStyle, IEquatable { + private readonly double[] _dashes; + /// /// Initializes a new instance of the class. /// @@ -17,12 +19,12 @@ namespace Avalonia.Media.Immutable /// The dash sequence offset. public ImmutableDashStyle(IEnumerable dashes, double offset) { - Dashes = (IReadOnlyList)dashes?.ToList() ?? Array.Empty(); + _dashes = dashes?.ToArray() ?? Array.Empty(); Offset = offset; } /// - public IReadOnlyList Dashes { get; } + public IReadOnlyList Dashes => _dashes; /// public double Offset { get; } @@ -56,9 +58,9 @@ namespace Avalonia.Media.Immutable var hashCode = 717868523; hashCode = hashCode * -1521134295 + Offset.GetHashCode(); - if (Dashes != null) + if (_dashes != null) { - foreach (var i in Dashes) + foreach (var i in _dashes) { hashCode = hashCode * -1521134295 + i.GetHashCode(); } @@ -69,7 +71,7 @@ namespace Avalonia.Media.Immutable private static bool SequenceEqual(IReadOnlyList left, IReadOnlyList right) { - if (left == right) + if (ReferenceEquals(left, right)) { return true; } diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs index e586eaf3a9..3256f4b11a 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; namespace Avalonia.Media.Immutable { @@ -23,7 +24,7 @@ namespace Avalonia.Media.Immutable ImmutableDashStyle dashStyle = null, PenLineCap lineCap = PenLineCap.Flat, PenLineJoin lineJoin = PenLineJoin.Miter, - double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit) + double miterLimit = 10.0) : this(new ImmutableSolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit) { } @@ -44,6 +45,8 @@ namespace Avalonia.Media.Immutable PenLineJoin lineJoin = PenLineJoin.Miter, double miterLimit = 10.0) { + Debug.Assert(!(brush is IMutableBrush)); + Brush = brush; Thickness = thickness; LineCap = lineCap; diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs index 010184ad3b..8e93ac580e 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.Immutable /// /// Fills an area with a solid color. /// - public readonly struct ImmutableSolidColorBrush : ISolidColorBrush, IEquatable + public class ImmutableSolidColorBrush : ISolidColorBrush, IEquatable { /// /// Initializes a new instance of the class. @@ -48,8 +48,9 @@ namespace Avalonia.Media.Immutable public bool Equals(ImmutableSolidColorBrush other) { - // ReSharper disable once CompareOfFloatsByEqualityOperator - return Color == other.Color && Opacity == other.Opacity; + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Color.Equals(other.Color) && Opacity.Equals(other.Opacity); } public override bool Equals(object obj) @@ -67,12 +68,12 @@ namespace Avalonia.Media.Immutable public static bool operator ==(ImmutableSolidColorBrush left, ImmutableSolidColorBrush right) { - return left.Equals(right); + return Equals(left, right); } public static bool operator !=(ImmutableSolidColorBrush left, ImmutableSolidColorBrush right) { - return !left.Equals(right); + return !Equals(left, right); } /// diff --git a/src/Avalonia.Visuals/Media/PolyLineSegment.cs b/src/Avalonia.Visuals/Media/PolyLineSegment.cs new file mode 100644 index 0000000000..55bfb33041 --- /dev/null +++ b/src/Avalonia.Visuals/Media/PolyLineSegment.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Avalonia.Collections; + +namespace Avalonia.Media +{ + /// + /// Represents a set of line segments defined by a points collection with each Point specifying the end point of a line segment. + /// + public sealed class PolyLineSegment : PathSegment + { + /// + /// Defines the property. + /// + public static readonly StyledProperty PointsProperty + = AvaloniaProperty.Register(nameof(Points)); + + /// + /// Gets or sets the points. + /// + /// + /// The points. + /// + public AvaloniaList Points + { + get => GetValue(PointsProperty); + set => SetValue(PointsProperty, value); + } + + /// + /// Initializes a new instance of the class. + /// + public PolyLineSegment() + { + Points = new Points(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The points. + public PolyLineSegment(IEnumerable points) : this() + { + Points.AddRange(points); + } + + protected internal override void ApplyTo(StreamGeometryContext ctx) + { + var points = Points; + if (points.Count > 0) + { + for (int i = 0; i < points.Count; i++) + { + ctx.LineTo(points[i]); + } + } + } + + public override string ToString() + => Points.Count >= 1 ? "L " + string.Join(" ", Points) : ""; + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs index 0a5c1f8db6..45b62b843b 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - /// Initializes a new instance of the class that represents an + /// Initializes a new instance of the class that represents an /// pop. /// public BitmapBlendModeNode() @@ -40,7 +40,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Determines if this draw operation equals another. /// - /// The opacity of the other draw operation. + /// the how to compare /// True if the draw operations are the same, otherwise false. /// /// The properties of the other draw operation are passed in as arguments to prevent diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs index 36aa08c2f9..d8e5baac97 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -257,7 +257,7 @@ namespace Avalonia.Rendering.SceneGraph if (childCount == 0 || wasVisited) { if ((wasVisited || FilterAndClip(node, ref clip)) && - (node.Visual is ICustomSimpleHitTest custom ? custom.HitTest(_point) : node.HitTest(_point))) + (node.Visual is ICustomHitTest custom ? custom.HitTest(_point) : node.HitTest(_point))) { _current = node.Visual; diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 56f05db04e..f2a09b815e 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -249,6 +249,8 @@ namespace Avalonia.Rendering.SceneGraph { var visualChildren = (IList) visual.VisualChildren; + node.TryPreallocateChildren(visualChildren.Count); + if (visualChildren.Count == 1) { var childNode = GetOrCreateChildNode(scene, visualChildren[0], node); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index d0439feed2..db6b606b41 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -358,6 +358,11 @@ namespace Avalonia.Rendering.SceneGraph internal void TryPreallocateChildren(int count) { + if (count == 0) + { + return; + } + EnsureChildrenCreated(count); } diff --git a/src/Avalonia.X11/TransparencyHelper.cs b/src/Avalonia.X11/TransparencyHelper.cs index 0578680136..2140b61b6f 100644 --- a/src/Avalonia.X11/TransparencyHelper.cs +++ b/src/Avalonia.X11/TransparencyHelper.cs @@ -10,7 +10,6 @@ namespace Avalonia.X11 private readonly X11Globals _globals; private WindowTransparencyLevel _currentLevel; private WindowTransparencyLevel _requestedLevel; - private bool _isCompositing; private bool _blurAtomsAreSet; public Action TransparencyLevelChanged { get; set; } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 5ac4c4c9d0..bcb655245a 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -30,7 +30,6 @@ namespace Avalonia.X11 ITopLevelImplWithTextInputMethod { private readonly AvaloniaX11Platform _platform; - private readonly IWindowImpl _popupParent; private readonly bool _popup; private readonly X11Info _x11; private XConfigureEvent? _configure; @@ -297,6 +296,30 @@ namespace Avalonia.X11 public Size ClientSize => new Size(_realSize.Width / RenderScaling, _realSize.Height / RenderScaling); + public Size? FrameSize + { + get + { + XGetWindowProperty(_x11.Display, _handle, _x11.Atoms._NET_FRAME_EXTENTS, IntPtr.Zero, + new IntPtr(4), false, (IntPtr)Atom.AnyPropertyType, out var _, + out var _, out var nitems, out var _, out var prop); + + if (nitems.ToInt64() != 4) + { + // Window hasn't been mapped by the WM yet, so can't get the extents. + return null; + } + + var data = (IntPtr*)prop.ToPointer(); + var extents = new Thickness(data[0].ToInt32(), data[2].ToInt32(), data[1].ToInt32(), data[3].ToInt32()); + XFree(prop); + + return new Size( + (_realSize.Width + extents.Left + extents.Right) / RenderScaling, + (_realSize.Height + extents.Top + extents.Bottom) / RenderScaling); + } + } + public double RenderScaling { get @@ -589,6 +612,13 @@ namespace Avalonia.X11 private void OnPropertyChange(IntPtr atom, bool hasValue) { + if (atom == _x11.Atoms._NET_FRAME_EXTENTS) + { + // Occurs once the window has been mapped, which is the earliest the extents + // can be retrieved, so invoke event to force update of TopLevel.FrameSize. + Resized.Invoke(ClientSize); + } + if (atom == _x11.Atoms._NET_WM_STATE) { WindowState state = WindowState.Normal; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 4bbb58e53e..5a1da9058a 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -15,7 +15,6 @@ namespace Avalonia.LinuxFramebuffer private readonly IOutputBackend _outputBackend; private readonly IInputBackend _inputBackend; - private bool _renderQueued; public IInputRoot InputRoot { get; private set; } public FramebufferToplevelImpl(IOutputBackend outputBackend, IInputBackend inputBackend) @@ -62,6 +61,7 @@ namespace Avalonia.LinuxFramebuffer } public Size ClientSize => ScaledSize; + public Size? FrameSize => null; public IMouseDevice MouseDevice => new MouseDevice(); public IPopupImpl CreatePopup() => null; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs index b69b151c3b..c35a3d1174 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs @@ -7,7 +7,6 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev internal class EvDevSingleTouchScreen : EvDevDeviceHandler { private readonly IScreenInfoProvider _screenInfo; - private readonly int _width, _height; private readonly Matrix _calibration; private input_absinfo _axisX; private input_absinfo _axisY; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index a6b70069c1..89f81a7649 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -132,7 +132,10 @@ public static class LinuxFramebufferPlatformExtensions { public static int StartLinuxFbDev(this T builder, string[] args, string fbdev = null, double scaling = 1) where T : AppBuilderBase, new() => - StartLinuxDirect(builder, args, new FbdevOutput(fbdev) {Scaling = scaling}); + StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: null) { Scaling = scaling }); + public static int StartLinuxFbDev(this T builder, string[] args, string fbdev, PixelFormat? format, double scaling) + where T : AppBuilderBase, new() => + StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: format) { Scaling = scaling }); public static int StartLinuxDrm(this T builder, string[] args, string card = null, double scaling = 1) where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card) {Scaling = scaling}); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs index ed59166eb8..87c7b64c26 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs @@ -41,6 +41,6 @@ namespace Avalonia.LinuxFramebuffer public PixelSize Size => new PixelSize((int)_varInfo.xres, (int) _varInfo.yres); public int RowBytes => (int) _fixedInfo.line_length; public Vector Dpi { get; } - public PixelFormat Format => _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888; + public PixelFormat Format => _varInfo.bits_per_pixel == 16 ? PixelFormat.Rgb565 : _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index dc44d2d55f..ee4125101c 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -16,7 +16,6 @@ namespace Avalonia.LinuxFramebuffer.Output public unsafe class DrmOutput : IGlOutputBackend, IGlPlatformSurface { private DrmCard _card; - private readonly EglGlPlatformSurface _eglPlatformSurface; public PixelSize PixelSize => _mode.Resolution; public double Scaling { get; set; } public IGlContext PrimaryContext => _deferredContext; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs index b83fe6cbe8..61f00b2795 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs @@ -16,16 +16,33 @@ namespace Avalonia.LinuxFramebuffer private IntPtr _mappedAddress; public double Scaling { get; set; } - public FbdevOutput(string fileName = null) + /// + /// Create a Linux frame buffer device output + /// + /// The frame buffer device name. + /// Defaults to the value in environment variable FRAMEBUFFER or /dev/fb0 when FRAMEBUFFER is not set + public FbdevOutput(string fileName = null) : this(fileName, null) { - fileName = fileName ?? Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0"; + } + + /// + /// Create a Linux frame buffer device output + /// + /// The frame buffer device name. + /// Defaults to the value in environment variable FRAMEBUFFER or /dev/fb0 when FRAMEBUFFER is not set + /// The required pixel format for the frame buffer. + /// A null value will leave the frame buffer in the current pixel format. + /// Otherwise sets the frame buffer to the required format + public FbdevOutput(string fileName, PixelFormat? format) + { + fileName ??= Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0"; _fd = NativeUnsafeMethods.open(fileName, 2, 0); if (_fd <= 0) throw new Exception("Error: " + Marshal.GetLastWin32Error()); try { - Init(); + Init(format); } catch { @@ -34,25 +51,28 @@ namespace Avalonia.LinuxFramebuffer } } - void Init() + void Init(PixelFormat? format) { fixed (void* pnfo = &_varInfo) { if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, pnfo)) throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error()); - SetBpp(); + if (format.HasValue) + { + SetBpp(format.Value); - if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo)) - _varInfo.transp = new fb_bitfield(); + if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo)) + _varInfo.transp = new fb_bitfield(); - NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo); + NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo); - if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, pnfo)) - throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error()); + if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, pnfo)) + throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error()); - if (_varInfo.bits_per_pixel != 32) - throw new Exception("Unable to set 32-bit display mode"); + if (_varInfo.bits_per_pixel != 32) + throw new Exception("Unable to set 32-bit display mode"); + } } fixed(void*pnfo = &_fixedInfo) if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_FSCREENINFO, pnfo)) @@ -70,17 +90,43 @@ namespace Avalonia.LinuxFramebuffer } } - void SetBpp() + void SetBpp(PixelFormat format) { - _varInfo.bits_per_pixel = 32; - _varInfo.grayscale = 0; - _varInfo.red = _varInfo.blue = _varInfo.green = _varInfo.transp = new fb_bitfield + switch (format) { - length = 8 - }; - _varInfo.green.offset = 8; - _varInfo.blue.offset = 16; - _varInfo.transp.offset = 24; + case PixelFormat.Rgba8888: + _varInfo.bits_per_pixel = 32; + _varInfo.grayscale = 0; + _varInfo.red = _varInfo.blue = _varInfo.green = _varInfo.transp = new fb_bitfield + { + length = 8 + }; + _varInfo.green.offset = 8; + _varInfo.blue.offset = 16; + _varInfo.transp.offset = 24; + break; + case PixelFormat.Bgra8888: + _varInfo.bits_per_pixel = 32; + _varInfo.grayscale = 0; + _varInfo.red = _varInfo.blue = _varInfo.green = _varInfo.transp = new fb_bitfield + { + length = 8 + }; + _varInfo.green.offset = 8; + _varInfo.red.offset = 16; + _varInfo.transp.offset = 24; + break; + case PixelFormat.Rgb565: + _varInfo.bits_per_pixel = 16; + _varInfo.grayscale = 0; + _varInfo.red = _varInfo.blue = _varInfo.green = _varInfo.transp = new fb_bitfield(); + _varInfo.red.length = 5; + _varInfo.green.offset = 5; + _varInfo.green.length = 6; + _varInfo.blue.offset = 11; + _varInfo.blue.length = 5; + break; + } } public string Id { get; private set; } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index 059b5650cb..4592b9c8b4 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -207,6 +207,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return true; } + if (types.IBrush.IsAssignableFrom(type)) + { + if (Color.TryParse(text, out Color color)) + { + var brushTypeRef = new XamlAstClrTypeReference(node, types.ImmutableSolidColorBrush, false); + + result = new XamlAstNewClrObjectNode(node, brushTypeRef, + types.ImmutableSolidColorBrushConstructorColor, + new List { new XamlConstantNode(node, types.UInt, color.ToUint32()) }); + + return true; + } + } + result = null; return false; } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index c4995b2de3..6dd3521183 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -80,7 +80,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType ColumnDefinitions { get; } public IXamlType Classes { get; } public IXamlMethod ClassesBindMethod { get; } - public IXamlProperty StyledElementClassesProperty { get; set; } + public IXamlProperty StyledElementClassesProperty { get; } + public IXamlType IBrush { get; } + public IXamlType ImmutableSolidColorBrush { get; } + public IXamlConstructor ImmutableSolidColorBrushConstructorColor { get; } public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg) { @@ -178,6 +181,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers .FindMethod( "BindClass", IDisposable, false, IStyledElement, cfg.WellKnownTypes.String, IBinding, cfg.WellKnownTypes.Object); + + IBrush = cfg.TypeSystem.GetType("Avalonia.Media.IBrush"); + ImmutableSolidColorBrush = cfg.TypeSystem.GetType("Avalonia.Media.Immutable.ImmutableSolidColorBrush"); + ImmutableSolidColorBrushConstructorColor = ImmutableSolidColorBrush.GetConstructor(new List { UInt }); } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 2c5c2acd52..2352b8b076 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -164,7 +164,7 @@ namespace Avalonia.Skia /// public void DrawLine(IPen pen, Point p1, Point p2) { - using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) + using (var paint = CreatePaint(_strokePaint, pen, new Rect(p1, p2).Normalize())) { if (paint.Paint is object) { @@ -177,10 +177,10 @@ namespace Avalonia.Skia public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) { var impl = (GeometryImpl) geometry; - var size = geometry.Bounds.Size; + var rect = geometry.Bounds; - using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper)) - using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper)) + using (var fill = brush != null ? CreatePaint(_fillPaint, brush, rect) : default(PaintWrapper)) + using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, rect) : default(PaintWrapper)) { if (fill.Paint != null) { @@ -354,7 +354,7 @@ namespace Avalonia.Skia if (brush != null) { - using (var paint = CreatePaint(_fillPaint, brush, rect.Rect.Size)) + using (var paint = CreatePaint(_fillPaint, brush, rect.Rect)) { if (isRounded) { @@ -397,7 +397,7 @@ namespace Avalonia.Skia if (pen?.Brush != null) { - using (var paint = CreatePaint(_strokePaint, pen, rect.Rect.Size)) + using (var paint = CreatePaint(_strokePaint, pen, rect.Rect)) { if (paint.Paint is object) { @@ -417,7 +417,7 @@ namespace Avalonia.Skia /// public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) { - using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds.Size)) + using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds)) { var textImpl = (FormattedTextImpl) text; textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering); @@ -427,7 +427,7 @@ namespace Avalonia.Skia /// public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { - using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Size)) + using (var paintWrapper = CreatePaint(_fillPaint, foreground, new Rect(glyphRun.Size))) { var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; @@ -537,7 +537,7 @@ namespace Avalonia.Skia var paint = new SKPaint(); Canvas.SaveLayer(paint); - _maskStack.Push(CreatePaint(paint, mask, bounds.Size, true)); + _maskStack.Push(CreatePaint(paint, mask, bounds, true)); } /// @@ -591,20 +591,21 @@ namespace Avalonia.Skia /// Configure paint wrapper for using gradient brush. /// /// Paint wrapper. - /// Target size. + /// Target bound rect. /// Gradient brush. - private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush) + private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Rect targetRect, IGradientBrush gradientBrush) { var tileMode = gradientBrush.SpreadMethod.ToSKShaderTileMode(); var stopColors = gradientBrush.GradientStops.Select(s => s.Color.ToSKColor()).ToArray(); var stopOffsets = gradientBrush.GradientStops.Select(s => (float)s.Offset).ToArray(); + var position = targetRect.Position.ToSKPoint(); switch (gradientBrush) { case ILinearGradientBrush linearGradient: { - var start = linearGradient.StartPoint.ToPixels(targetSize).ToSKPoint(); - var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint(); + var start = position + linearGradient.StartPoint.ToPixels(targetRect.Size).ToSKPoint(); + var end = position + linearGradient.EndPoint.ToPixels(targetRect.Size).ToSKPoint(); // would be nice to cache these shaders possibly? using (var shader = @@ -617,10 +618,10 @@ namespace Avalonia.Skia } case IRadialGradientBrush radialGradient: { - var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint(); - var radius = (float)(radialGradient.Radius * targetSize.Width); + var center = position + radialGradient.Center.ToPixels(targetRect.Size).ToSKPoint(); + var radius = (float)(radialGradient.Radius * targetRect.Width); - var origin = radialGradient.GradientOrigin.ToPixels(targetSize).ToSKPoint(); + var origin = position + radialGradient.GradientOrigin.ToPixels(targetRect.Size).ToSKPoint(); if (origin.Equals(center)) { @@ -665,7 +666,7 @@ namespace Avalonia.Skia } case IConicGradientBrush conicGradient: { - var center = conicGradient.Center.ToPixels(targetSize).ToSKPoint(); + var center = position + conicGradient.Center.ToPixels(targetRect.Size).ToSKPoint(); // Skia's default is that angle 0 is from the right hand side of the center point // but we are matching CSS where the vertical point above the center is 0. @@ -867,10 +868,10 @@ namespace Avalonia.Skia /// /// The paint to wrap. /// Source brush. - /// Target size. + /// Target rect. /// Optional dispose of the supplied paint. /// Paint wrapper for given brush. - internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize, bool disposePaint = false) + internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Rect targetRect, bool disposePaint = false) { var paintWrapper = new PaintWrapper(paint, disposePaint); @@ -889,7 +890,7 @@ namespace Avalonia.Skia if (brush is IGradientBrush gradient) { - ConfigureGradientBrush(ref paintWrapper, targetSize, gradient); + ConfigureGradientBrush(ref paintWrapper, targetRect, gradient); return paintWrapper; } @@ -909,7 +910,7 @@ namespace Avalonia.Skia if (tileBrush != null && tileBrushImage != null) { - ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage); + ConfigureTileBrush(ref paintWrapper, targetRect.Size, tileBrush, tileBrushImage); } else { @@ -924,10 +925,10 @@ namespace Avalonia.Skia /// /// The paint to wrap. /// Source pen. - /// Target size. + /// Target rect. /// Optional dispose of the supplied paint. /// - private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Size targetSize, bool disposePaint = false) + private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Rect targetRect, bool disposePaint = false) { // In Skia 0 thickness means - use hairline rendering // and for us it means - there is nothing rendered. @@ -936,7 +937,7 @@ namespace Avalonia.Skia return default; } - var rv = CreatePaint(paint, pen.Brush, targetSize, disposePaint); + var rv = CreatePaint(paint, pen.Brush, targetRect, disposePaint); paint.IsStroke = true; paint.StrokeWidth = (float) pen.Thickness; diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 5e630e54a6..3eca42faa9 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Avalonia.Media; using Avalonia.Platform; @@ -175,7 +176,8 @@ namespace Avalonia.Skia foreach (var line in _skiaLines.Where(l => (l.Start + l.Length) > index && - lastIndex >= l.Start)) + lastIndex >= l.Start && + !l.IsEmptyTrailingLine)) { int lineEndIndex = line.Start + (line.Length > 0 ? line.Length - 1 : 0); @@ -276,9 +278,9 @@ namespace Avalonia.Skia if (fb != null) { - //TODO: figure out how to get the brush size + //TODO: figure out how to get the brush rect currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb, - new Size()); + default); } else { @@ -466,7 +468,8 @@ namespace Avalonia.Skia for (int i = line.Start; i < line.Start + line.TextLength; i++) { - float w = _paint.MeasureText(Text[i].ToString()); + var c = Text[i]; + var w = line.IsEmptyTrailingLine ? 0 :_paint.MeasureText(Text[i].ToString()); _rects.Add(new Rect( prevRight, @@ -611,6 +614,7 @@ namespace Avalonia.Skia lastLine.Width = lastLineWidth; lastLine.Height = _lineHeight; lastLine.Top = curY; + lastLine.IsEmptyTrailingLine = true; _skiaLines.Add(lastLine); @@ -713,6 +717,7 @@ namespace Avalonia.Skia public int TextLength; public float Top; public float Width; + public bool IsEmptyTrailingLine; }; private struct FBrushRange diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index aa86df7c23..32818dfdd2 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -16,11 +16,12 @@ namespace Avalonia.Skia /// Surfaces. /// Created render target or if it fails. ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable surfaces); - + /// /// Creates an offscreen render target surface /// /// size in pixels + /// current Skia render session ISkiaSurface TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs b/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs index 22c998df93..e4b2405290 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs @@ -34,10 +34,10 @@ namespace Avalonia.Direct2D1.Media { var wrapper = clientDrawingEffect as BrushWrapper; - // TODO: Work out how to get the size below rather than passing new Size(). + // TODO: Work out how to get the rect below rather than passing default. var brush = (wrapper == null) ? _foreground : - _context.CreateBrush(wrapper.Brush, new Size()).PlatformBrush; + _context.CreateBrush(wrapper.Brush, default).PlatformBrush; _renderTarget.DrawGlyphRun( new RawVector2 { X = baselineOriginX, Y = baselineOriginY }, diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index af35934785..9336c9a7bb 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -192,7 +192,7 @@ namespace Avalonia.Direct2D1.Media { using (var d2dSource = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext)) using (var sourceBrush = new BitmapBrush(_deviceContext, d2dSource.Value)) - using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect.Size)) + using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect)) using (var geometry = new SharpDX.Direct2D1.RectangleGeometry(Direct2D1Platform.Direct2D1Factory, destRect.ToDirect2D())) { if (d2dOpacityMask.PlatformBrush != null) @@ -217,9 +217,7 @@ namespace Avalonia.Direct2D1.Media { if (pen != null) { - var size = new Rect(p1, p2).Size; - - using (var d2dBrush = CreateBrush(pen.Brush, size)) + using (var d2dBrush = CreateBrush(pen.Brush, new Rect(p1, p2).Normalize())) using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext)) { if (d2dBrush.PlatformBrush != null) @@ -245,7 +243,7 @@ namespace Avalonia.Direct2D1.Media { if (brush != null) { - using (var d2dBrush = CreateBrush(brush, geometry.Bounds.Size)) + using (var d2dBrush = CreateBrush(brush, geometry.Bounds)) { if (d2dBrush.PlatformBrush != null) { @@ -257,7 +255,7 @@ namespace Avalonia.Direct2D1.Media if (pen != null) { - using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen).Size)) + using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen))) using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext)) { if (d2dBrush.PlatformBrush != null) @@ -282,7 +280,7 @@ namespace Avalonia.Direct2D1.Media if (brush != null) { - using (var b = CreateBrush(brush, rect.Size)) + using (var b = CreateBrush(brush, rect)) { if (b.PlatformBrush != null) { @@ -311,7 +309,7 @@ namespace Avalonia.Direct2D1.Media if (pen?.Brush != null) { - using (var wrapper = CreateBrush(pen.Brush, rect.Size)) + using (var wrapper = CreateBrush(pen.Brush, rect)) using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext)) { if (wrapper.PlatformBrush != null) @@ -349,7 +347,7 @@ namespace Avalonia.Direct2D1.Media { var impl = (FormattedTextImpl)text; - using (var brush = CreateBrush(foreground, impl.Bounds.Size)) + using (var brush = CreateBrush(foreground, impl.Bounds)) using (var renderer = new AvaloniaTextRenderer(this, _deviceContext, brush.PlatformBrush)) { if (brush.PlatformBrush != null) @@ -367,7 +365,7 @@ namespace Avalonia.Direct2D1.Media /// The glyph run. public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { - using (var brush = CreateBrush(foreground, glyphRun.Size)) + using (var brush = CreateBrush(foreground, new Rect(glyphRun.Size))) { var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; @@ -458,9 +456,9 @@ namespace Avalonia.Direct2D1.Media /// Creates a Direct2D brush wrapper for a Avalonia brush. /// /// The avalonia brush. - /// The size of the brush's target area. + /// The brush's target area. /// The Direct2D brush wrapper. - public BrushImpl CreateBrush(IBrush brush, Size destinationSize) + public BrushImpl CreateBrush(IBrush brush, Rect destinationRect) { var solidColorBrush = brush as ISolidColorBrush; var linearGradientBrush = brush as ILinearGradientBrush; @@ -475,11 +473,11 @@ namespace Avalonia.Direct2D1.Media } else if (linearGradientBrush != null) { - return new LinearGradientBrushImpl(linearGradientBrush, _deviceContext, destinationSize); + return new LinearGradientBrushImpl(linearGradientBrush, _deviceContext, destinationRect); } else if (radialGradientBrush != null) { - return new RadialGradientBrushImpl(radialGradientBrush, _deviceContext, destinationSize); + return new RadialGradientBrushImpl(radialGradientBrush, _deviceContext, destinationRect); } else if (conicGradientBrush != null) { @@ -492,7 +490,7 @@ namespace Avalonia.Direct2D1.Media imageBrush, _deviceContext, (BitmapImpl)imageBrush.Source.PlatformImpl.Item, - destinationSize); + destinationRect.Size); } else if (visualBrush != null) { @@ -523,7 +521,7 @@ namespace Avalonia.Direct2D1.Media visualBrush, _deviceContext, new D2DBitmapImpl(intermediate.Bitmap), - destinationSize); + destinationRect.Size); } } } @@ -574,7 +572,7 @@ namespace Avalonia.Direct2D1.Media ContentBounds = PrimitiveExtensions.RectangleInfinite, MaskTransform = PrimitiveExtensions.Matrix3x2Identity, Opacity = 1, - OpacityBrush = CreateBrush(mask, bounds.Size).PlatformBrush + OpacityBrush = CreateBrush(mask, bounds).PlatformBrush }; var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_deviceContext); _deviceContext.PushLayer(ref parameters, layer); diff --git a/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs index 0e63d4cc03..69b45455ac 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs @@ -8,7 +8,7 @@ namespace Avalonia.Direct2D1.Media public LinearGradientBrushImpl( ILinearGradientBrush brush, SharpDX.Direct2D1.RenderTarget target, - Size destinationSize) + Rect destinationRect) { if (brush.GradientStops.Count == 0) { @@ -21,8 +21,9 @@ namespace Avalonia.Direct2D1.Media Position = (float)s.Offset }).ToArray(); - var startPoint = brush.StartPoint.ToPixels(destinationSize); - var endPoint = brush.EndPoint.ToPixels(destinationSize); + var position = destinationRect.Position; + var startPoint = position + brush.StartPoint.ToPixels(destinationRect.Size); + var endPoint = position + brush.EndPoint.ToPixels(destinationRect.Size); using (var stops = new SharpDX.Direct2D1.GradientStopCollection( target, diff --git a/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs index 1fca6d4e33..7dcfd7e1e0 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs @@ -8,7 +8,7 @@ namespace Avalonia.Direct2D1.Media public RadialGradientBrushImpl( IRadialGradientBrush brush, SharpDX.Direct2D1.RenderTarget target, - Size destinationSize) + Rect destinationRect) { if (brush.GradientStops.Count == 0) { @@ -21,12 +21,13 @@ namespace Avalonia.Direct2D1.Media Position = (float)s.Offset }).ToArray(); - var centerPoint = brush.Center.ToPixels(destinationSize); - var gradientOrigin = brush.GradientOrigin.ToPixels(destinationSize) - centerPoint; + var position = destinationRect.Position; + var centerPoint = position + brush.Center.ToPixels(destinationRect.Size); + var gradientOrigin = position + brush.GradientOrigin.ToPixels(destinationRect.Size) - centerPoint; // Note: Direct2D supports RadiusX and RadiusY but Cairo backend supports only Radius property - var radiusX = brush.Radius * destinationSize.Width; - var radiusY = brush.Radius * destinationSize.Height; + var radiusX = brush.Radius * destinationRect.Width; + var radiusY = brush.Radius * destinationRect.Height; using (var stops = new SharpDX.Direct2D1.GradientStopCollection( target, diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index fe1d625efb..73e46b9e13 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -100,6 +100,7 @@ namespace Avalonia.Win32.Interop.Wpf } Size ITopLevelImpl.ClientSize => _finalSize; + Size? ITopLevelImpl.FrameSize => null; IMouseDevice ITopLevelImpl.MouseDevice => _mouse; double ITopLevelImpl.RenderScaling => PresentationSource.FromVisual(this)?.CompositionTarget?.TransformToDevice.M11 ?? 1; diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index c137926e4c..ad409810b8 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -745,6 +745,26 @@ namespace Avalonia.Win32.Interop WM_DISPATCH_WORK_ITEM = WM_USER, } + public enum DwmWindowAttribute : uint + { + DWMWA_NCRENDERING_ENABLED = 1, + DWMWA_NCRENDERING_POLICY, + DWMWA_TRANSITIONS_FORCEDISABLED, + DWMWA_ALLOW_NCPAINT, + DWMWA_CAPTION_BUTTON_BOUNDS, + DWMWA_NONCLIENT_RTL_LAYOUT, + DWMWA_FORCE_ICONIC_REPRESENTATION, + DWMWA_FLIP3D_POLICY, + DWMWA_EXTENDED_FRAME_BOUNDS, + DWMWA_HAS_ICONIC_BITMAP, + DWMWA_DISALLOW_PEEK, + DWMWA_EXCLUDED_FROM_PEEK, + DWMWA_CLOAK, + DWMWA_CLOAKED, + DWMWA_FREEZE_REPRESENTATION, + DWMWA_LAST + }; + public enum MapVirtualKeyMapTypes : uint { MAPVK_VK_TO_VSC = 0x00, @@ -1388,6 +1408,9 @@ namespace Avalonia.Win32.Interop [DllImport("dwmapi.dll")] public static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins); + [DllImport("dwmapi.dll")] + public static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out RECT pvAttribute, int cbAttribute); + [DllImport("dwmapi.dll")] public static extern int DwmIsCompositionEnabled(out bool enabled); diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs index 2aa82436f6..1c3c959acf 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs @@ -17,7 +17,6 @@ namespace Avalonia.Win32.WinRT.Composition class WinUICompositorConnection : IRenderTimer { private readonly EglContext _syncContext; - private IntPtr _queue; private ICompositor _compositor; private ICompositor2 _compositor2; private ICompositor5 _compositor5; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs index d886b67241..8a340aac5e 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs @@ -117,7 +117,7 @@ namespace Avalonia.Win32 { var visual = window.Renderer.HitTestFirst(position, _owner as Window, x => { - if (x is IInputElement ie && !ie.IsHitTestVisible) + if (x is IInputElement ie && (!ie.IsHitTestVisible || !ie.IsVisible)) { return false; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 082aca1109..646a6f5739 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -213,6 +213,21 @@ namespace Avalonia.Win32 } } + public Size? FrameSize + { + get + { + if (DwmIsCompositionEnabled(out var compositionEnabled) != 0 || !compositionEnabled) + { + GetWindowRect(_hwnd, out var rcWindow); + return new Size(rcWindow.Width, rcWindow.Height) / RenderScaling; + } + + DwmGetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_EXTENDED_FRAME_BOUNDS, out var rect, Marshal.SizeOf(typeof(RECT))); + return new Size(rect.Width, rect.Height) / RenderScaling; + } + } + public IScreenImpl Screen { get; } public IPlatformHandle Handle { get; private set; } @@ -900,7 +915,7 @@ namespace Avalonia.Win32 IntPtr.Zero, rcWindow.left, rcWindow.top, rcClient.Width, rcClient.Height, - SetWindowPosFlags.SWP_FRAMECHANGED); + SetWindowPosFlags.SWP_FRAMECHANGED | SetWindowPosFlags.SWP_NOACTIVATE); if (_isClientAreaExtended && WindowState != WindowState.FullScreen) { diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 36a70ea410..0371a7759a 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -91,6 +91,7 @@ namespace Avalonia.iOS } public Size ClientSize => new Size(_view.Bounds.Width, _view.Bounds.Height); + public Size? FrameSize => null; public double RenderScaling => _view.ContentScaleFactor; public IEnumerable Surfaces { get; set; } public Action Input { get; set; } diff --git a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs index fe718ec32b..58bd7a42c3 100644 --- a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs +++ b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs @@ -9,6 +9,8 @@ using Avalonia.UnitTests; using Avalonia.Data; using Xunit; using Avalonia.Animation.Easings; +using System.Threading; +using System.Reactive.Linq; namespace Avalonia.Animation.UnitTests { @@ -176,5 +178,271 @@ namespace Avalonia.Animation.UnitTests clock.Step(TimeSpan.FromSeconds(0.100d)); Assert.Equal(border.Width, 300d); } + + [Fact(Skip = "See #6111")] + public void Dispose_Subscription_Should_Stop_Animation() + { + var keyframe1 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 200d), + }, + Cue = new Cue(1d) + }; + + var keyframe2 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 100d), + }, + Cue = new Cue(0d) + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(10), + Delay = TimeSpan.FromSeconds(0), + DelayBetweenIterations = TimeSpan.FromSeconds(0), + IterationCount = new IterationCount(1), + Children = + { + keyframe2, + keyframe1 + } + }; + + var border = new Border() + { + Height = 100d, + Width = 50d + }; + var propertyChangedCount = 0; + var animationCompletedCount = 0; + border.PropertyChanged += (sender, e) => + { + if (e.Property == Control.WidthProperty) + { + propertyChangedCount++; + } + }; + + var clock = new TestClock(); + var disposable = animation.Apply(border, clock, Observable.Return(true), () => animationCompletedCount++); + + Assert.Equal(0, propertyChangedCount); + + clock.Step(TimeSpan.FromSeconds(0)); + Assert.Equal(0, animationCompletedCount); + Assert.Equal(1, propertyChangedCount); + + disposable.Dispose(); + + // Clock ticks should be ignored after Dispose + clock.Step(TimeSpan.FromSeconds(5)); + clock.Step(TimeSpan.FromSeconds(6)); + clock.Step(TimeSpan.FromSeconds(7)); + + // On animation disposing (cancellation) on completed is not invoked (is it expected?) + Assert.Equal(0, animationCompletedCount); + // Initial property changed before cancellation + animation value removal. + Assert.Equal(2, propertyChangedCount); + } + + [Fact] + public void Do_Not_Run_Cancelled_Animation() + { + var keyframe1 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 200d), + }, + Cue = new Cue(1d) + }; + + var keyframe2 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 100d), + }, + Cue = new Cue(0d) + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(10), + Delay = TimeSpan.FromSeconds(0), + DelayBetweenIterations = TimeSpan.FromSeconds(0), + IterationCount = new IterationCount(1), + Children = + { + keyframe2, + keyframe1 + } + }; + + var border = new Border() + { + Height = 100d, + Width = 100d + }; + var propertyChangedCount = 0; + border.PropertyChanged += (sender, e) => + { + if (e.Property == Control.WidthProperty) + { + propertyChangedCount++; + } + }; + + var clock = new TestClock(); + var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.Cancel(); + var animationRun = animation.RunAsync(border, clock, cancellationTokenSource.Token); + + clock.Step(TimeSpan.FromSeconds(10)); + Assert.Equal(0, propertyChangedCount); + Assert.True(animationRun.IsCompleted); + } + + [Fact(Skip = "See #6111")] + public void Cancellation_Should_Stop_Animation() + { + var keyframe1 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 200d), + }, + Cue = new Cue(1d) + }; + + var keyframe2 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 100d), + }, + Cue = new Cue(0d) + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(10), + Delay = TimeSpan.FromSeconds(0), + DelayBetweenIterations = TimeSpan.FromSeconds(0), + IterationCount = new IterationCount(1), + Children = + { + keyframe2, + keyframe1 + } + }; + + var border = new Border() + { + Height = 100d, + Width = 50d + }; + var propertyChangedCount = 0; + border.PropertyChanged += (sender, e) => + { + if (e.Property == Control.WidthProperty) + { + propertyChangedCount++; + } + }; + + var clock = new TestClock(); + var cancellationTokenSource = new CancellationTokenSource(); + var animationRun = animation.RunAsync(border, clock, cancellationTokenSource.Token); + + Assert.Equal(0, propertyChangedCount); + + clock.Step(TimeSpan.FromSeconds(0)); + Assert.False(animationRun.IsCompleted); + Assert.Equal(1, propertyChangedCount); + + cancellationTokenSource.Cancel(); + clock.Step(TimeSpan.FromSeconds(1)); + clock.Step(TimeSpan.FromSeconds(2)); + clock.Step(TimeSpan.FromSeconds(3)); + //Assert.Equal(2, propertyChangedCount); + + animationRun.Wait(); + + clock.Step(TimeSpan.FromSeconds(6)); + Assert.True(animationRun.IsCompleted); + Assert.Equal(2, propertyChangedCount); + } + + [Fact] + public void Cancellation_Of_Completed_Animation_Does_Not_Fail() + { + var keyframe1 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 200d), + }, + Cue = new Cue(1d) + }; + + var keyframe2 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 100d), + }, + Cue = new Cue(0d) + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(10), + Delay = TimeSpan.FromSeconds(0), + DelayBetweenIterations = TimeSpan.FromSeconds(0), + IterationCount = new IterationCount(1), + Children = + { + keyframe2, + keyframe1 + } + }; + + var border = new Border() + { + Height = 100d, + Width = 50d + }; + var propertyChangedCount = 0; + border.PropertyChanged += (sender, e) => + { + if (e.Property == Control.WidthProperty) + { + propertyChangedCount++; + } + }; + + var clock = new TestClock(); + var cancellationTokenSource = new CancellationTokenSource(); + var animationRun = animation.RunAsync(border, clock, cancellationTokenSource.Token); + + Assert.Equal(0, propertyChangedCount); + + clock.Step(TimeSpan.FromSeconds(0)); + Assert.False(animationRun.IsCompleted); + Assert.Equal(1, propertyChangedCount); + + clock.Step(TimeSpan.FromSeconds(10)); + Assert.True(animationRun.IsCompleted); + Assert.Equal(2, propertyChangedCount); + + cancellationTokenSource.Cancel(); + animationRun.Wait(); + } } } diff --git a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs new file mode 100644 index 0000000000..7626be7760 --- /dev/null +++ b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs @@ -0,0 +1,103 @@ +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; +using Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Benchmarks +{ + internal class NullDrawingContextImpl : IDrawingContextImpl + { + public void Dispose() + { + } + + public Matrix Transform { get; set; } + + public void Clear(Color color) + { + } + + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + { + } + + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + { + } + + public void DrawLine(IPen pen, Point p1, Point p2) + { + } + + public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) + { + } + + public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default) + { + } + + public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) + { + } + + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + { + } + + public IDrawingContextLayerImpl CreateLayer(Size size) + { + return null; + } + + public void PushClip(Rect clip) + { + } + + public void PushClip(RoundedRect clip) + { + } + + public void PopClip() + { + } + + public void PushOpacity(double opacity) + { + } + + public void PopOpacity() + { + } + + public void PushOpacityMask(IBrush mask, Rect bounds) + { + } + + public void PopOpacityMask() + { + } + + public void PushGeometryClip(IGeometryImpl clip) + { + } + + public void PopGeometryClip() + { + } + + public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + { + } + + public void PopBitmapBlendMode() + { + } + + public void Custom(ICustomDrawOperation custom) + { + } + } +} diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 8792cb5cb1..876a0de643 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -18,12 +18,12 @@ namespace Avalonia.Benchmarks public IGeometryImpl CreateEllipseGeometry(Rect rect) { - throw new NotImplementedException(); + return new MockStreamGeometryImpl(); } public IGeometryImpl CreateLineGeometry(Point p1, Point p2) { - throw new NotImplementedException(); + return new MockStreamGeometryImpl(); } public IGeometryImpl CreateRectangleGeometry(Rect rect) diff --git a/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs b/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs new file mode 100644 index 0000000000..b0db806afa --- /dev/null +++ b/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs @@ -0,0 +1,53 @@ +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.Platform; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Rendering +{ + [MemoryDiagnoser] + public class ShapeRendering + { + private readonly DrawingContext _drawingContext; + private readonly Line _lineFill; + private readonly Line _lineFillAndStroke; + private readonly Line _lineNoBrushes; + private readonly Line _lineStroke; + + public ShapeRendering() + { + _lineNoBrushes = new Line(); + _lineStroke = new Line { Stroke = new SolidColorBrush() }; + _lineFill = new Line { Fill = new SolidColorBrush() }; + _lineFillAndStroke = new Line { Stroke = new SolidColorBrush(), Fill = new SolidColorBrush() }; + + _drawingContext = new DrawingContext(new NullDrawingContextImpl(), true); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new NullRenderingPlatform()); + } + + [Benchmark] + public void Render_Line_NoBrushes() + { + _lineNoBrushes.Render(_drawingContext); + } + + [Benchmark] + public void Render_Line_WithStroke() + { + _lineStroke.Render(_drawingContext); + } + + [Benchmark] + public void Render_Line_WithFill() + { + _lineFill.Render(_drawingContext); + } + + [Benchmark] + public void Render_Line_WithFillAndStroke() + { + _lineFillAndStroke.Render(_drawingContext); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 8f9c7fdb0b..cb2fd11175 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -201,6 +201,65 @@ namespace Avalonia.Controls.UnitTests } - } + } + + [Fact] + public void Close_Window_On_Alt_F4_When_ComboBox_Is_Focus() + { + var inputManagerMock = new Moq.Mock(); + var services = TestServices.StyledWindow.With(inputManager: inputManagerMock.Object); + + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + + window.KeyDown += (s, e) => + { + if (e.Handled == false + && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) == true + && e.Key == Key.F4 ) + { + e.Handled = true; + window.Close(); + } + }; + + var count = 0; + + var target = new ComboBox + { + Items = new[] { new Canvas() }, + SelectedIndex = 0, + Template = GetTemplate(), + }; + + window.Content = target; + + + window.Closing += + (sender, e) => + { + count++; + }; + + window.Show(); + + target.Focus(); + + _helper.Down(target); + _helper.Up(target); + Assert.True(target.IsDropDownOpen); + + target.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + KeyModifiers = KeyModifiers.Alt, + Key = Key.F4 + }); + + + Assert.Equal(1, count); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index cea77bb7c9..72ba3ab273 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1066,7 +1066,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Auto_Expanding_In_Style_Should_Not_Break_Range_Selection() { - /// Issue #2980. + // Issue #2980. using (Application()) { var target = new DerivedTreeView diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index e8311b79ac..6b9921d83d 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -279,10 +279,11 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var parent = Mock.Of(); + var parent = new Window(); var renderer = new Mock(); var target = new Window(CreateImpl(renderer)); + parent.Show(); target.ShowDialog(parent); renderer.Verify(x => x.Start(), Times.Once); @@ -294,10 +295,11 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var parent = Mock.Of(); + var parent = new Window(); var target = new Window(); var raised = false; + parent.Show(); target.Opened += (s, e) => raised = true; target.ShowDialog(parent); @@ -326,14 +328,15 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var parent = new Mock(); + var parent = new Window(); var windowImpl = new Mock(); windowImpl.SetupProperty(x => x.Closed); windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1); + parent.Show(); var target = new Window(windowImpl.Object); - var task = target.ShowDialog(parent.Object); + var task = target.ShowDialog(parent); windowImpl.Object.Closed(); @@ -366,14 +369,16 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var parent = new Mock(); + var parent = new Window(); var windowImpl = new Mock(); windowImpl.SetupProperty(x => x.Closed); windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1); + parent.Show(); + var target = new Window(windowImpl.Object); - var task = target.ShowDialog(parent.Object); + var task = target.ShowDialog(parent); windowImpl.Object.Closed(); await task; @@ -381,12 +386,128 @@ namespace Avalonia.Controls.UnitTests var openedRaised = false; target.Opened += (s, e) => openedRaised = true; - var ex = await Assert.ThrowsAsync(() => target.ShowDialog(parent.Object)); + var ex = await Assert.ThrowsAsync(() => target.ShowDialog(parent)); Assert.Equal("Cannot re-show a closed window.", ex.Message); Assert.False(openedRaised); } } + [Fact] + public void Calling_Show_With_Closed_Parent_Window_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parent = new Window(); + var target = new Window(); + + parent.Close(); + + var ex = Assert.Throws(() => target.Show(parent)); + Assert.Equal("Cannot show a window with a closed parent.", ex.Message); + } + } + + [Fact] + public async Task Calling_ShowDialog_With_Closed_Parent_Window_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parent = new Window(); + var target = new Window(); + + parent.Close(); + + var ex = await Assert.ThrowsAsync(() => target.ShowDialog(parent)); + Assert.Equal("Cannot show a window with a closed owner.", ex.Message); + } + } + + [Fact] + public void Calling_Show_With_Invisible_Parent_Window_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parent = new Window(); + var target = new Window(); + + var ex = Assert.Throws(() => target.Show(parent)); + Assert.Equal("Cannot show window with non-visible parent.", ex.Message); + } + } + + [Fact] + public async Task Calling_ShowDialog_With_Invisible_Parent_Window_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parent = new Window(); + var target = new Window(); + + var ex = await Assert.ThrowsAsync(() => target.ShowDialog(parent)); + Assert.Equal("Cannot show window with non-visible parent.", ex.Message); + } + } + + [Fact] + public void Calling_Show_With_Self_As_Parent_Window_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Window(); + + var ex = Assert.Throws(() => target.Show(target)); + Assert.Equal("A Window cannot be its own parent.", ex.Message); + } + } + + [Fact] + public async Task Calling_ShowDialog_With_Self_As_Parent_Window_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Window(); + + var ex = await Assert.ThrowsAsync(() => target.ShowDialog(target)); + Assert.Equal("A Window cannot be its own owner.", ex.Message); + } + } + + [Fact] + public void Hiding_Parent_Window_Should_Close_Children() + { + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + { + var parent = new Window(); + var child = new Window(); + + parent.Show(); + child.Show(parent); + + parent.Hide(); + + Assert.False(parent.IsVisible); + Assert.False(child.IsVisible); + } + } + + [Fact] + public void Hiding_Parent_Window_Should_Close_Dialog_Children() + { + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + { + var parent = new Window(); + var child = new Window(); + + parent.Show(); + child.ShowDialog(parent); + + parent.Hide(); + + Assert.False(parent.IsVisible); + Assert.False(child.IsVisible); + } + } + [Fact] public void Window_Should_Be_Centered_When_WindowStartupLocation_Is_CenterScreen() { @@ -686,6 +807,7 @@ namespace Avalonia.Controls.UnitTests protected override void Show(Window window) { var owner = new Window(); + owner.Show(); window.ShowDialog(owner); } } diff --git a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs index df0a077c7f..7730cee78c 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs @@ -1,5 +1,9 @@ -using Avalonia.Input.Raw; +using System; +using System.Windows.Input; +using Avalonia.Controls; +using Avalonia.Input.Raw; using Avalonia.Interactivity; +using Avalonia.UnitTests; using Moq; using Xunit; @@ -86,5 +90,45 @@ namespace Avalonia.Input.UnitTests focused.Verify(x => x.RaiseEvent(It.IsAny())); } + + [Fact] + public void Can_Change_KeyBindings_In_Keybinding_Event_Handler() + { + var target = new KeyboardDevice(); + var button = new Button(); + var root = new TestRoot(button); + var raised = 0; + + button.KeyBindings.Add(new KeyBinding + { + Gesture = new KeyGesture(Key.O, KeyModifiers.Control), + Command = new DelegateCommand(() => + { + button.KeyBindings.Clear(); + ++raised; + }), + }); + + target.SetFocusedElement(button, NavigationMethod.Pointer, 0); + target.ProcessRawEvent( + new RawKeyEventArgs( + target, + 0, + root, + RawKeyEventType.KeyDown, + Key.O, + RawInputModifiers.Control)); + + Assert.Equal(1, raised); + } + + private class DelegateCommand : ICommand + { + private readonly Action _action; + public DelegateCommand(Action action) => _action = action; + public event EventHandler CanExecuteChanged; + public bool CanExecute(object parameter) => true; + public void Execute(object parameter) => _action(); + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index a97cb69c86..e7f0230254 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -33,7 +33,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions DelayedBinding.ApplyBindings(border); - var brush = (SolidColorBrush)border.Background; + var brush = (ISolidColorBrush)border.Background; Assert.Equal(0xff506070, brush.Color.ToUint32()); } @@ -80,7 +80,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions DelayedBinding.ApplyBindings(border); - var brush = (SolidColorBrush)border.Background; + var brush = (ISolidColorBrush)border.Background; Assert.Equal(0xff506070, brush.Color.ToUint32()); } @@ -108,7 +108,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions DelayedBinding.ApplyBindings(border); - var brush = (SolidColorBrush)border.Background; + var brush = (ISolidColorBrush)border.Background; Assert.Equal(0xff506070, brush.Color.ToUint32()); } @@ -140,7 +140,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions DelayedBinding.ApplyBindings(border); - var brush = (SolidColorBrush)border.Background; + var brush = (ISolidColorBrush)border.Background; Assert.Equal(0xff506070, brush.Color.ToUint32()); } @@ -212,7 +212,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var button = window.FindControl