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 e40ec12461..7df9b76425 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -29,10 +29,12 @@ public: IAvnMenu* _mainMenu; bool _shown; + bool _inResize; WindowBaseImpl(IAvnWindowBaseEvents* events, IAvnGlContext* gl) { _shown = false; + _inResize = false; _mainMenu = nullptr; BaseEvents = events; _glContext = gl; @@ -54,6 +56,8 @@ public: virtual HRESULT ObtainNSWindowHandle(void** ret) override { + START_COM_CALL; + if (ret == nullptr) { return E_POINTER; @@ -66,6 +70,8 @@ public: virtual HRESULT ObtainNSWindowHandleRetained(void** ret) override { + START_COM_CALL; + if (ret == nullptr) { return E_POINTER; @@ -78,6 +84,8 @@ public: virtual HRESULT ObtainNSViewHandle(void** ret) override { + START_COM_CALL; + if (ret == nullptr) { return E_POINTER; @@ -90,6 +98,8 @@ public: virtual HRESULT ObtainNSViewHandleRetained(void** ret) override { + START_COM_CALL; + if (ret == nullptr) { return E_POINTER; @@ -107,23 +117,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 +152,8 @@ public: virtual HRESULT Hide () override { + START_COM_CALL; + @autoreleasepool { if(Window != nullptr) @@ -152,6 +168,8 @@ public: virtual HRESULT Activate () override { + START_COM_CALL; + @autoreleasepool { if(Window != nullptr) @@ -166,6 +184,8 @@ public: virtual HRESULT SetTopMost (bool value) override { + START_COM_CALL; + @autoreleasepool { [Window setLevel: value ? NSFloatingWindowLevel : NSNormalWindowLevel]; @@ -176,11 +196,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 +214,8 @@ public: virtual HRESULT GetClientSize(AvnSize* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -219,6 +246,8 @@ public: virtual HRESULT GetScaling (double* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -237,6 +266,8 @@ public: virtual HRESULT SetMinMaxSize (AvnSize minSize, AvnSize maxSize) override { + START_COM_CALL; + @autoreleasepool { [Window setMinSize: ToNSSize(minSize)]; @@ -248,6 +279,15 @@ public: virtual HRESULT Resize(double x, double y) override { + if(_inResize) + { + return S_OK; + } + + _inResize = true; + + START_COM_CALL; + @autoreleasepool { auto maxSize = [Window maxSize]; @@ -273,13 +313,19 @@ public: y = maxSize.height; } - if(!_shown) + @try { - BaseEvents->Resized(AvnSize{x,y}); + if(!_shown) + { + BaseEvents->Resized(AvnSize{x,y}); + } + + [Window setContentSize:NSSize{x, y}]; + } + @finally + { + _inResize = false; } - - [StandardContainer setFrameSize:NSSize{x,y}]; - [Window setContentSize:NSSize{x, y}]; return S_OK; } @@ -287,6 +333,8 @@ public: virtual HRESULT Invalidate (AvnRect rect) override { + START_COM_CALL; + @autoreleasepool { [View setNeedsDisplayInRect:[View frame]]; @@ -297,6 +345,8 @@ public: virtual HRESULT SetMainMenu(IAvnMenu* menu) override { + START_COM_CALL; + _mainMenu = menu; auto nativeMenu = dynamic_cast(menu); @@ -315,6 +365,8 @@ public: virtual HRESULT BeginMoveDrag () override { + START_COM_CALL; + @autoreleasepool { auto lastEvent = [View lastMouseDownEvent]; @@ -332,11 +384,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) @@ -357,6 +413,8 @@ public: virtual HRESULT SetPosition (AvnPoint point) override { + START_COM_CALL; + @autoreleasepool { lastPositionSet = point; @@ -368,6 +426,8 @@ public: virtual HRESULT PointToClient (AvnPoint point, AvnPoint* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -386,6 +446,8 @@ public: virtual HRESULT PointToScreen (AvnPoint point, AvnPoint* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -403,12 +465,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); @@ -438,6 +504,8 @@ public: virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ppv) override { + START_COM_CALL; + if(View == NULL) return E_FAIL; *ppv = [renderTarget createSurfaceRenderTarget]; @@ -446,6 +514,8 @@ public: virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost** retOut) override { + START_COM_CALL; + if(View == NULL) return E_FAIL; *retOut = ::CreateNativeControlHost(View); @@ -454,6 +524,8 @@ public: virtual HRESULT SetBlurEnabled (bool enable) override { + START_COM_CALL; + [StandardContainer ShowBlur:enable]; return S_OK; @@ -463,6 +535,8 @@ public: IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) override { + START_COM_CALL; + auto item = TryGetPasteboardItem(clipboard); [item setString:@"" forType:GetAvnCustomDataType()]; if(item == nil) @@ -564,6 +638,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]; @@ -590,8 +669,10 @@ private: virtual HRESULT Show (bool activate) override { + START_COM_CALL; + @autoreleasepool - { + { WindowBaseImpl::Show(activate); HideOrShowTrafficLights(); @@ -602,6 +683,8 @@ private: virtual HRESULT SetEnabled (bool enable) override { + START_COM_CALL; + @autoreleasepool { [Window setEnabled:enable]; @@ -611,6 +694,8 @@ private: virtual HRESULT SetParent (IAvnWindow* parent) override { + START_COM_CALL; + @autoreleasepool { if(parent == nullptr) @@ -687,6 +772,7 @@ private: } _lastWindowState = state; + _actualWindowState = state; WindowEvents->WindowStateChanged(state); } } @@ -722,6 +808,8 @@ private: virtual HRESULT SetCanResize(bool value) override { + START_COM_CALL; + @autoreleasepool { _canResize = value; @@ -732,6 +820,8 @@ private: virtual HRESULT SetDecorations(SystemDecorations value) override { + START_COM_CALL; + @autoreleasepool { auto currentWindowState = _lastWindowState; @@ -797,6 +887,8 @@ private: virtual HRESULT SetTitle (char* utf8title) override { + START_COM_CALL; + @autoreleasepool { _lastTitle = [NSString stringWithUTF8String:(const char*)utf8title]; @@ -808,6 +900,8 @@ private: virtual HRESULT SetTitleBarColor(AvnColor color) override { + START_COM_CALL; + @autoreleasepool { float a = (float)color.Alpha / 255.0f; @@ -837,6 +931,8 @@ private: virtual HRESULT GetWindowState (AvnWindowState*ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -870,86 +966,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; - - [Window setTitlebarAppearsTransparent:true]; - - auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + _isClientAreaExtended = enable; - if (wantsTitleBar) + if(enable) { - [StandardContainer ShowTitleBar:true]; - } - else - { - [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 () @@ -978,8 +1099,15 @@ private: virtual HRESULT SetWindowState (AvnWindowState state) override { + START_COM_CALL; + @autoreleasepool { + if(Window == nullptr) + { + return S_OK; + } + if(_actualWindowState == state) { return S_OK; @@ -1164,6 +1292,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [_blurBehind setWantsLayer:true]; _blurBehind.hidden = true; + [_blurBehind setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [_content setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [self addSubview:_blurBehind]; [self addSubview:_content]; @@ -1199,9 +1330,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _settingSize = true; [super setFrameSize:newSize]; - [_blurBehind setFrameSize:newSize]; - [_content setFrameSize:newSize]; - auto window = objc_cast([self window]); // TODO get actual titlebar size @@ -1217,6 +1345,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [_titleBarMaterial setFrame:tbar]; tbar.size.height = height < 1 ? 0 : 1; [_titleBarUnderline setFrame:tbar]; + _settingSize = false; } @@ -1926,7 +2055,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { if(![self windowShouldClose:self]) return; } - + [self close]; } @@ -2262,11 +2391,12 @@ protected: virtual HRESULT Resize(double x, double y) override { + START_COM_CALL; + @autoreleasepool { if (Window != nullptr) { - [StandardContainer setFrameSize:NSSize{x,y}]; [Window setContentSize:NSSize{x, y}]; [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))]; diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml index d75622f1fb..2f422e2d64 100644 --- a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml +++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml @@ -2,7 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=netstandard" x:Class="ControlCatalog.Pages.NumericUpDownPage"> - + Numeric up-down control Numeric up-down control provides a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel. @@ -68,24 +69,36 @@ + - - Usage of NumericUpDown: - + + + + + + + + + + - NumericUpDown with Validation Errors: - - - - - - + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs index 31749edf08..5856194430 100644 --- a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs +++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs @@ -32,11 +32,26 @@ namespace ControlCatalog.Pages private FormatObject _selectedFormat; private IList _spinnerLocations; + private double _doubleValue; + private decimal _decimalValue; + public NumbersPageViewModel() { SelectedFormat = Formats.FirstOrDefault(); } + public double DoubleValue + { + get { return _doubleValue; } + set { this.RaiseAndSetIfChanged(ref _doubleValue, value); } + } + + public decimal DecimalValue + { + get { return _decimalValue; } + set { this.RaiseAndSetIfChanged(ref _decimalValue, value); } + } + public IList Formats { get 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/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index 5681214222..2c7f34c5be 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -454,6 +454,28 @@ namespace Avalonia.Collections } } + /// + /// Ensures that the capacity of the list is at least . + /// + /// The capacity. + public void EnsureCapacity(int capacity) + { + // Adapted from List implementation. + var currentCapacity = _inner.Capacity; + + if (currentCapacity < capacity) + { + var newCapacity = currentCapacity == 0 ? 4 : currentCapacity * 2; + + if (newCapacity < capacity) + { + newCapacity = capacity; + } + + _inner.Capacity = newCapacity; + } + } + /// /// Removes an item from the collection. /// @@ -633,24 +655,6 @@ namespace Avalonia.Collections /// Delegate[] INotifyCollectionChangedDebug.GetCollectionChangedSubscribers() => _collectionChanged?.GetInvocationList(); - private void EnsureCapacity(int capacity) - { - // Adapted from List implementation. - var currentCapacity = _inner.Capacity; - - if (currentCapacity < capacity) - { - var newCapacity = currentCapacity == 0 ? 4 : currentCapacity * 2; - - if (newCapacity < capacity) - { - newCapacity = capacity; - } - - _inner.Capacity = newCapacity; - } - } - /// /// Raises the event with an add action. /// diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 1ea369954a..596cbf1d7e 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -224,6 +224,34 @@ namespace Avalonia.Utilities } } + /// + /// Clamps a value between a minimum and maximum value. + /// + /// The value. + /// The minimum value. + /// The maximum value. + /// The clamped value. + public static decimal Clamp(decimal val, decimal min, decimal max) + { + if (min > max) + { + ThrowCannotBeGreaterThanException(min, max); + } + + if (val < min) + { + return min; + } + else if (val > max) + { + return max; + } + else + { + return val; + } + } + /// /// Clamps a value between a minimum and maximum value. /// @@ -281,8 +309,8 @@ namespace Avalonia.Utilities { return angle * 2 * Math.PI; } - - private static void ThrowCannotBeGreaterThanException(double min, double max) + + private static void ThrowCannotBeGreaterThanException(T min, T max) { throw new ArgumentException($"{min} cannot be greater than {max}."); } diff --git a/src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs b/src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs index 47d1ac1c0b..38e18b57de 100644 --- a/src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs +++ b/src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs @@ -340,10 +340,30 @@ namespace Avalonia.Controls.Utils internal static PropertyInfo GetPropertyOrIndexer(this Type type, string propertyPath, out object[] index) { index = null; + // Return the default value of GetProperty if the first character is not an indexer token. if (string.IsNullOrEmpty(propertyPath) || propertyPath[0] != LeftIndexerToken) { - // Return the default value of GetProperty if the first character is not an indexer token. - return type.GetProperty(propertyPath); + var property = type.GetProperty(propertyPath); + if (property != null) + { + return property; + } + + // GetProperty does not return inherited interface properties, + // so we need to enumerate them manually. + if (type.IsInterface) + { + foreach (var typeInterface in type.GetInterfaces()) + { + property = type.GetProperty(propertyPath); + if (property != null) + { + return property; + } + } + } + + return null; } if (propertyPath.Length < 2 || propertyPath[propertyPath.Length - 1] != RightIndexerToken) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 1abf4fd6ff..bb133900d9 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -4,14 +4,38 @@ 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. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. +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. 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. -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 +Total Issues: 39 diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index dd2109e6cd..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() { @@ -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/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/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index abfbc038eb..3a83d9ed7c 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -45,10 +45,18 @@ namespace Avalonia.Controls /// /// Defines the property. /// + [Obsolete] public static readonly DirectProperty CultureInfoProperty = AvaloniaProperty.RegisterDirect(nameof(CultureInfo), o => o.CultureInfo, (o, v) => o.CultureInfo = v, CultureInfo.CurrentCulture); + /// + /// Defines the property. + /// + public static readonly DirectProperty NumberFormatProperty = + AvaloniaProperty.RegisterDirect(nameof(NumberFormat), o => o.NumberFormat, + (o, v) => o.NumberFormat = v, NumberFormatInfo.CurrentInfo); + /// /// Defines the property. /// @@ -58,8 +66,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty IncrementProperty = - AvaloniaProperty.Register(nameof(Increment), 1.0d, coerce: OnCoerceIncrement); + public static readonly StyledProperty IncrementProperty = + AvaloniaProperty.Register(nameof(Increment), 1.0m, coerce: OnCoerceIncrement); /// /// Defines the property. @@ -70,14 +78,14 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty MaximumProperty = - AvaloniaProperty.Register(nameof(Maximum), double.MaxValue, coerce: OnCoerceMaximum); + public static readonly StyledProperty MaximumProperty = + AvaloniaProperty.Register(nameof(Maximum), decimal.MaxValue, coerce: OnCoerceMaximum); /// /// Defines the property. /// - public static readonly StyledProperty MinimumProperty = - AvaloniaProperty.Register(nameof(Minimum), double.MinValue, coerce: OnCoerceMinimum); + public static readonly StyledProperty MinimumProperty = + AvaloniaProperty.Register(nameof(Minimum), decimal.MinValue, coerce: OnCoerceMinimum); /// /// Defines the property. @@ -96,8 +104,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty ValueProperty = - AvaloniaProperty.RegisterDirect(nameof(Value), updown => updown.Value, + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect(nameof(Value), updown => updown.Value, (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); /// @@ -106,7 +114,6 @@ namespace Avalonia.Controls public static readonly StyledProperty WatermarkProperty = AvaloniaProperty.Register(nameof(Watermark)); - /// /// Defines the property. /// @@ -121,7 +128,7 @@ namespace Avalonia.Controls private IDisposable _textBoxTextChangedSubscription; - private double _value; + private decimal _value; private string _text; private bool _internalValueSet; private bool _clipValueToMinMax; @@ -129,7 +136,8 @@ namespace Avalonia.Controls private bool _isTextChangedFromUI; private CultureInfo _cultureInfo; private NumberStyles _parsingNumberStyle = NumberStyles.Any; - + private NumberFormatInfo _numberFormat; + /// /// Gets the Spinner template part. /// @@ -179,10 +187,25 @@ namespace Avalonia.Controls /// /// Gets or sets the current CultureInfo. /// + [Obsolete("CultureInfo is obsolete, please use NumberFormat instead.")] public CultureInfo CultureInfo { get { return _cultureInfo; } - set { SetAndRaise(CultureInfoProperty, ref _cultureInfo, value); } + set + { + SetAndRaise(CultureInfoProperty, ref _cultureInfo, value); + //Set and Raise the NumberFormatProperty when CultureInfo is changed. + SetAndRaise(NumberFormatProperty, ref _numberFormat, value?.NumberFormat); + } + } + + /// + /// Gets or sets the current NumberFormatInfo + /// + public NumberFormatInfo NumberFormat + { + get { return _numberFormat; } + set { SetAndRaise(NumberFormatProperty, ref _numberFormat, value); } } /// @@ -197,7 +220,7 @@ namespace Avalonia.Controls /// /// Gets or sets the amount in which to increment the . /// - public double Increment + public decimal Increment { get { return GetValue(IncrementProperty); } set { SetValue(IncrementProperty, value); } @@ -215,7 +238,7 @@ namespace Avalonia.Controls /// /// Gets or sets the maximum allowed value. /// - public double Maximum + public decimal Maximum { get { return GetValue(MaximumProperty); } set { SetValue(MaximumProperty, value); } @@ -224,7 +247,7 @@ namespace Avalonia.Controls /// /// Gets or sets the minimum allowed value. /// - public double Minimum + public decimal Minimum { get { return GetValue(MinimumProperty); } set { SetValue(MinimumProperty, value); } @@ -251,7 +274,7 @@ namespace Avalonia.Controls /// /// Gets or sets the value. /// - public double Value + public decimal Value { get { return _value; } set @@ -270,7 +293,6 @@ namespace Avalonia.Controls set { SetValue(WatermarkProperty, value); } } - /// /// Gets or sets the horizontal alignment of the content within the control. /// @@ -311,6 +333,7 @@ namespace Avalonia.Controls static NumericUpDown() { CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged); + NumberFormatProperty.Changed.Subscribe(OnNumberFormatChanged); FormatStringProperty.Changed.Subscribe(FormatStringChanged); IncrementProperty.Changed.Subscribe(IncrementChanged); IsReadOnlyProperty.Changed.Subscribe(OnIsReadOnlyChanged); @@ -397,6 +420,19 @@ namespace Avalonia.Controls } } + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnNumberFormatChanged(NumberFormatInfo oldValue, NumberFormatInfo newValue) + { + if (IsInitialized) + { + SyncTextAndValueProperties(false, null); + } + } + /// /// Called when the property value changed. /// @@ -415,7 +451,7 @@ namespace Avalonia.Controls /// /// The old value. /// The new value. - protected virtual void OnIncrementChanged(double oldValue, double newValue) + protected virtual void OnIncrementChanged(decimal oldValue, decimal newValue) { if (IsInitialized) { @@ -438,7 +474,7 @@ namespace Avalonia.Controls /// /// The old value. /// The new value. - protected virtual void OnMaximumChanged(double oldValue, double newValue) + protected virtual void OnMaximumChanged(decimal oldValue, decimal newValue) { if (IsInitialized) { @@ -455,7 +491,7 @@ namespace Avalonia.Controls /// /// The old value. /// The new value. - protected virtual void OnMinimumChanged(double oldValue, double newValue) + protected virtual void OnMinimumChanged(decimal oldValue, decimal newValue) { if (IsInitialized) { @@ -485,7 +521,7 @@ namespace Avalonia.Controls /// /// The old value. /// The new value. - protected virtual void OnValueChanged(double oldValue, double newValue) + protected virtual void OnValueChanged(decimal oldValue, decimal newValue) { if (!_internalValueSet && IsInitialized) { @@ -501,7 +537,7 @@ namespace Avalonia.Controls /// Called when the property has to be coerced. /// /// The value. - protected virtual double OnCoerceIncrement(double baseValue) + protected virtual decimal OnCoerceIncrement(decimal baseValue) { return baseValue; } @@ -510,7 +546,7 @@ namespace Avalonia.Controls /// Called when the property has to be coerced. /// /// The value. - protected virtual double OnCoerceMaximum(double baseValue) + protected virtual decimal OnCoerceMaximum(decimal baseValue) { return Math.Max(baseValue, Minimum); } @@ -519,7 +555,7 @@ namespace Avalonia.Controls /// Called when the property has to be coerced. /// /// The value. - protected virtual double OnCoerceMinimum(double baseValue) + protected virtual decimal OnCoerceMinimum(decimal baseValue) { return Math.Min(baseValue, Maximum); } @@ -528,7 +564,7 @@ namespace Avalonia.Controls /// Called when the property has to be coerced. /// /// The value. - protected virtual double OnCoerceValue(double baseValue) + protected virtual decimal OnCoerceValue(decimal baseValue) { return baseValue; } @@ -562,7 +598,7 @@ namespace Avalonia.Controls /// /// The old value. /// The new value. - protected virtual void RaiseValueChangedEvent(double oldValue, double newValue) + protected virtual void RaiseValueChangedEvent(decimal oldValue, decimal newValue) { var e = new NumericUpDownValueChangedEventArgs(ValueChangedEvent, oldValue, newValue); RaiseEvent(e); @@ -571,9 +607,9 @@ namespace Avalonia.Controls /// /// Converts the formatted text to a value. /// - private double ConvertTextToValue(string text) + private decimal ConvertTextToValue(string text) { - double result = 0; + decimal result = 0; if (string.IsNullOrEmpty(text)) { @@ -609,10 +645,10 @@ namespace Avalonia.Controls //Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind. if (FormatString.Contains("{0")) { - return string.Format(CultureInfo, FormatString, Value); + return string.Format(NumberFormat, FormatString, Value); } - return Value.ToString(FormatString, CultureInfo); + return Value.ToString(FormatString, NumberFormat); } /// @@ -674,6 +710,20 @@ namespace Avalonia.Controls } } + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnNumberFormatChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (NumberFormatInfo)e.OldValue; + var newValue = (NumberFormatInfo)e.NewValue; + upDown.OnNumberFormatChanged(oldValue, newValue); + } + } + /// /// Called when the property value changed. /// @@ -682,8 +732,8 @@ namespace Avalonia.Controls { if (e.Sender is NumericUpDown upDown) { - var oldValue = (double)e.OldValue; - var newValue = (double)e.NewValue; + var oldValue = (decimal)e.OldValue; + var newValue = (decimal)e.NewValue; upDown.OnIncrementChanged(oldValue, newValue); } } @@ -724,8 +774,8 @@ namespace Avalonia.Controls { if (e.Sender is NumericUpDown upDown) { - var oldValue = (double)e.OldValue; - var newValue = (double)e.NewValue; + var oldValue = (decimal)e.OldValue; + var newValue = (decimal)e.NewValue; upDown.OnMaximumChanged(oldValue, newValue); } } @@ -738,8 +788,8 @@ namespace Avalonia.Controls { if (e.Sender is NumericUpDown upDown) { - var oldValue = (double)e.OldValue; - var newValue = (double)e.NewValue; + var oldValue = (decimal)e.OldValue; + var newValue = (decimal)e.NewValue; upDown.OnMinimumChanged(oldValue, newValue); } } @@ -766,13 +816,13 @@ namespace Avalonia.Controls { if (e.Sender is NumericUpDown upDown) { - var oldValue = (double)e.OldValue; - var newValue = (double)e.NewValue; + var oldValue = (decimal)e.OldValue; + var newValue = (decimal)e.NewValue; upDown.OnValueChanged(oldValue, newValue); } } - private void SetValueInternal(double value) + private void SetValueInternal(decimal value) { _internalValueSet = true; try @@ -785,7 +835,7 @@ namespace Avalonia.Controls } } - private static double OnCoerceMaximum(IAvaloniaObject instance, double value) + private static decimal OnCoerceMaximum(IAvaloniaObject instance, decimal value) { if (instance is NumericUpDown upDown) { @@ -795,7 +845,7 @@ namespace Avalonia.Controls return value; } - private static double OnCoerceMinimum(IAvaloniaObject instance, double value) + private static decimal OnCoerceMinimum(IAvaloniaObject instance, decimal value) { if (instance is NumericUpDown upDown) { @@ -805,7 +855,7 @@ namespace Avalonia.Controls return value; } - private static double OnCoerceIncrement(IAvaloniaObject instance, double value) + private static decimal OnCoerceIncrement(IAvaloniaObject instance, decimal value) { if (instance is NumericUpDown upDown) { @@ -977,23 +1027,23 @@ namespace Avalonia.Controls return parsedTextIsValid; } - private double ConvertTextToValueCore(string currentValueText, string text) + private decimal ConvertTextToValueCore(string currentValueText, string text) { - double result; + decimal result; if (IsPercent(FormatString)) { - result = decimal.ToDouble(ParsePercent(text, CultureInfo)); + result = ParsePercent(text, NumberFormat); } else { // Problem while converting new text - if (!double.TryParse(text, ParsingNumberStyle, CultureInfo, out var outputValue)) + if (!decimal.TryParse(text, ParsingNumberStyle, NumberFormat, out var outputValue)) { var shouldThrow = true; // Check if CurrentValueText is also failing => it also contains special characters. ex : 90° - if (!double.TryParse(currentValueText, ParsingNumberStyle, CultureInfo, out var _)) + if (!decimal.TryParse(currentValueText, ParsingNumberStyle, NumberFormat, out var _)) { // extract non-digit characters var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c)); @@ -1006,7 +1056,7 @@ namespace Avalonia.Controls text = text.Replace(character.ToString(), string.Empty); } // if without the special characters, parsing is good, do not throw - if (double.TryParse(text, ParsingNumberStyle, CultureInfo, out outputValue)) + if (decimal.TryParse(text, ParsingNumberStyle, NumberFormat, out outputValue)) { shouldThrow = false; } @@ -1023,7 +1073,7 @@ namespace Avalonia.Controls return result; } - private void ValidateMinMax(double value) + private void ValidateMinMax(decimal value) { if (value < Minimum) { diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs index e994ffdd15..9b467d682c 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs @@ -4,13 +4,13 @@ namespace Avalonia.Controls { public class NumericUpDownValueChangedEventArgs : RoutedEventArgs { - public NumericUpDownValueChangedEventArgs(RoutedEvent routedEvent, double oldValue, double newValue) : base(routedEvent) + public NumericUpDownValueChangedEventArgs(RoutedEvent routedEvent, decimal oldValue, decimal newValue) : base(routedEvent) { OldValue = oldValue; NewValue = newValue; } - public double OldValue { get; } - public double NewValue { get; } + public decimal OldValue { get; } + public decimal NewValue { get; } } } diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index a397608aba..52c8273b6c 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -1,19 +1,33 @@ using System; using System.Collections.Specialized; -using System.Linq; -using System.Resources; using Avalonia.Media; using Avalonia.Rendering; -using Avalonia.Utilities; using Avalonia.VisualTree; +#nullable enable + namespace Avalonia.Controls.Primitives { - // TODO: Need to track position of adorned elements and move the adorner if they move. + /// + /// Represents a surface for showing adorners. + /// Adorners are always on top of the adorned element and are positioned to stay relative to the adorned element. + /// + /// + /// TODO: Need to track position of adorned elements and move the adorner if they move. + /// public class AdornerLayer : Canvas, ICustomSimpleHitTest { - public static readonly AttachedProperty AdornedElementProperty = - AvaloniaProperty.RegisterAttached("AdornedElement"); + /// + /// Allows for getting and setting of the adorned element. + /// + public static readonly AttachedProperty AdornedElementProperty = + AvaloniaProperty.RegisterAttached("AdornedElement"); + + /// + /// Allows for controlling clipping of the adorner. + /// + public static readonly AttachedProperty IsClipEnabledProperty = + AvaloniaProperty.RegisterAttached("IsClipEnabled", true); private static readonly AttachedProperty s_adornedElementInfoProperty = AvaloniaProperty.RegisterAttached("AdornedElementInfo"); @@ -28,7 +42,7 @@ namespace Avalonia.Controls.Primitives Children.CollectionChanged += ChildrenCollectionChanged; } - public static Visual GetAdornedElement(Visual adorner) + public static Visual? GetAdornedElement(Visual adorner) { return adorner.GetValue(AdornedElementProperty); } @@ -38,12 +52,19 @@ namespace Avalonia.Controls.Primitives adorner.SetValue(AdornedElementProperty, adorned); } - public static AdornerLayer GetAdornerLayer(IVisual visual) + public static AdornerLayer? GetAdornerLayer(IVisual visual) + { + return visual.FindAncestorOfType()?.AdornerLayer; + } + + public static bool GetIsClipEnabled(Visual adorner) { - return visual.GetVisualAncestors() - .OfType() - .FirstOrDefault() - ?.AdornerLayer; + return adorner.GetValue(IsClipEnabledProperty); + } + + public static void SetIsClipEnabled(Visual adorner, bool isClipEnabled) + { + adorner.SetValue(IsClipEnabledProperty, isClipEnabled); } protected override Size MeasureOverride(Size availableSize) @@ -70,12 +91,13 @@ namespace Avalonia.Controls.Primitives foreach (var child in Children) { var info = child.GetValue(s_adornedElementInfoProperty); + var isClipEnabled = child.GetValue(IsClipEnabledProperty); if (info != null && info.Bounds.HasValue) { child.RenderTransform = new MatrixTransform(info.Bounds.Value.Transform); child.RenderTransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute); - UpdateClip(child, info.Bounds.Value); + UpdateClip(child, info.Bounds.Value, isClipEnabled); child.Arrange(info.Bounds.Value.Bounds); } else @@ -87,16 +109,23 @@ namespace Avalonia.Controls.Primitives return finalSize; } - private static void AdornedElementChanged(AvaloniaPropertyChangedEventArgs e) + private static void AdornedElementChanged(AvaloniaPropertyChangedEventArgs e) { var adorner = (Visual)e.Sender; - var adorned = (Visual)e.NewValue; + var adorned = e.NewValue.GetValueOrDefault(); var layer = adorner.GetVisualParent(); layer?.UpdateAdornedElement(adorner, adorned); } - private void UpdateClip(IControl control, TransformedBounds bounds) + private void UpdateClip(IControl control, TransformedBounds bounds, bool isEnabled) { + if (!isEnabled) + { + control.Clip = null; + + return; + } + if (!(control.Clip is RectangleGeometry clip)) { clip = new RectangleGeometry(); @@ -129,13 +158,13 @@ namespace Avalonia.Controls.Primitives InvalidateArrange(); } - private void UpdateAdornedElement(Visual adorner, Visual adorned) + private void UpdateAdornedElement(Visual adorner, Visual? adorned) { var info = adorner.GetValue(s_adornedElementInfoProperty); if (info != null) { - info.Subscription.Dispose(); + info.Subscription!.Dispose(); if (adorned == null) { @@ -163,7 +192,7 @@ namespace Avalonia.Controls.Primitives private class AdornedElementInfo { - public IDisposable Subscription { get; set; } + public IDisposable? Subscription { get; set; } public TransformedBounds? Bounds { get; set; } } 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/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index 348922b71d..91141058e4 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -56,6 +56,26 @@ namespace Avalonia.Controls.Primitives { } + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + if (_lastPoint.HasValue) + { + var ev = new VectorEventArgs + { + RoutedEvent = DragCompletedEvent, + Vector = _lastPoint.Value, + }; + + _lastPoint = null; + + RaiseEvent(ev); + } + + PseudoClasses.Remove(":pressed"); + + base.OnPointerCaptureLost(e); + } + protected override void OnPointerMoved(PointerEventArgs e) { if (_lastPoint.HasValue) 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/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 3aef2abac5..91411cbd3b 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -145,7 +145,7 @@ namespace Avalonia.Controls (o, v) => o.UndoLimit = v, unsetValue: -1); - struct UndoRedoState : IEquatable + readonly struct UndoRedoState : IEquatable { public string Text { get; } public int CaretPosition { get; } 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.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index f3b60f07be..f740be44a2 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -114,7 +114,7 @@ namespace Avalonia.Native { var visual = (_inputRoot as Window).Renderer.HitTestFirst(e.Position, _inputRoot 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/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index c917902dc3..39a4c3004c 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -5,6 +5,7 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Task 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. @@ -73,4 +74,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWr 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: 74 +Total Issues: 75 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/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/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/AvaloniaXamlIlBindingPathParser.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs index 24cded1d22..890cbb69bf 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs @@ -9,7 +9,6 @@ using XamlX.Ast; using XamlX.Transform; using XamlX.Transform.Transformers; using XamlX.TypeSystem; - using XamlParseException = XamlX.XamlParseException; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers @@ -21,6 +20,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers if (node is XamlAstObjectNode binding && binding.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)) { var convertedNode = ConvertLongFormPropertiesToBindingExpressionNode(context, binding); + var foundPath = false; if (binding.Arguments.Count > 0 && binding.Arguments[0] is XamlAstTextNode bindingPathText) { @@ -32,9 +32,18 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers nodes.Insert(nodes.TakeWhile(x => x is BindingExpressionGrammar.ITransformNode).Count(), convertedNode); } - binding.Arguments[0] = new ParsedBindingPathNode(bindingPathText, context.GetAvaloniaTypes().CompiledBindingPath, nodes); + if (nodes.Count == 1 && nodes[0] is BindingExpressionGrammar.EmptyExpressionNode) + { + binding.Arguments.RemoveAt(0); + } + else + { + binding.Arguments[0] = new ParsedBindingPathNode(bindingPathText, context.GetAvaloniaTypes().CompiledBindingPath, nodes); + foundPath = true; + } } - else + + if (!foundPath) { var bindingPathAssignment = binding.Children.OfType() .FirstOrDefault(v => v.Property.GetClrProperty().Name == "Path"); @@ -44,12 +53,19 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers var reader = new CharacterReader(pathValue.Text.AsSpan()); var (nodes, _) = BindingExpressionGrammar.Parse(ref reader); - if (convertedNode != null) + if (nodes.Count == 1 && nodes[0] is BindingExpressionGrammar.EmptyExpressionNode) { - nodes.Insert(nodes.TakeWhile(x => x is BindingExpressionGrammar.ITransformNode).Count(), convertedNode); + bindingPathAssignment.Values.RemoveAt(0); } + else + { + if (convertedNode != null) + { + nodes.Insert(nodes.TakeWhile(x => x is BindingExpressionGrammar.ITransformNode).Count(), convertedNode); + } - bindingPathAssignment.Values[0] = new ParsedBindingPathNode(pathValue, context.GetAvaloniaTypes().CompiledBindingPath, nodes); + bindingPathAssignment.Values[0] = new ParsedBindingPathNode(pathValue, context.GetAvaloniaTypes().CompiledBindingPath, nodes); + } } } } 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/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.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 6f549a4ffa..8a61458030 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -938,6 +938,110 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void SupportsEmptyPath() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(typeof(TestDataContext).FullName, textBlock.Text); + } + } + + [Fact] + public void SupportsEmptyPathWithStringFormat() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal("bar-" + typeof(TestDataContext).FullName, textBlock.Text); + } + } + + [Fact] + public void SupportsDotPath() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(typeof(TestDataContext).FullName, textBlock.Text); + } + } + + [Fact] + public void SupportsExplicitDotPathWithStringFormat() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal("bar-" + typeof(TestDataContext).FullName, textBlock.Text); + } + } + void Throws(string type, Action cb) { try 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