diff --git a/Avalonia.sln b/Avalonia.sln index 38f8e5f720..e8d5034fb0 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -206,6 +206,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -1921,6 +1923,30 @@ Global {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhone.Build.0 = Release|Any CPU {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|iPhone.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|iPhone.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|Any CPU.Build.0 = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhone.ActiveCfg = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhone.Build.0 = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Documentation/build.md b/Documentation/build.md index 56b028206d..8c2ef57b54 100644 --- a/Documentation/build.md +++ b/Documentation/build.md @@ -36,7 +36,7 @@ Avalonia requires [CastXML](https://github.com/CastXML/CastXML) for XML processi On macOS: ``` -brew install castxml +brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb ``` On Debian based Linux (Debian, Ubuntu, Mint, etc): diff --git a/build.sh b/build.sh index 40b1c225a6..a40e00f815 100755 --- a/build.sh +++ b/build.sh @@ -67,6 +67,8 @@ else fi fi +export PATH=$DOTNET_DIRECTORY:$PATH + echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]} diff --git a/build/CoreLibraries.props b/build/CoreLibraries.props index d61268173d..2b54ee3f56 100644 --- a/build/CoreLibraries.props +++ b/build/CoreLibraries.props @@ -11,6 +11,7 @@ + diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 38d99db5c9..f9bfaf0b47 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -22,6 +22,9 @@ struct IAvnGlSurfaceRenderTarget; struct IAvnGlSurfaceRenderingSession; struct IAvnMenu; struct IAvnMenuItem; +struct IAvnStringArray; +struct IAvnDndResultCallback; +struct IAvnGCHandleDeallocatorCallback; struct IAvnMenuEvents; enum SystemDecorations { @@ -130,6 +133,22 @@ enum AvnInputModifiers XButton2MouseButton = 256 }; +enum class AvnDragDropEffects +{ + None = 0, + Copy = 1, + Move = 2, + Link = 4, +}; + +enum class AvnDragEventType +{ + Enter, + Over, + Leave, + Drop +}; + enum AvnWindowState { Normal, @@ -188,7 +207,7 @@ enum AvnMenuItemToggleType AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown { public: - virtual HRESULT Initialize() = 0; + virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) = 0; virtual IAvnMacOptions* GetMacOptions() = 0; virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv) = 0; virtual HRESULT CreatePopup (IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv) = 0; @@ -196,6 +215,7 @@ public: virtual HRESULT CreateSystemDialogs (IAvnSystemDialogs** ppv) = 0; virtual HRESULT CreateScreens (IAvnScreens** ppv) = 0; virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0; + virtual HRESULT CreateDndClipboard(IAvnClipboard** ppv) = 0; virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0; virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) = 0; virtual HRESULT SetAppMenu(IAvnMenu* menu) = 0; @@ -236,6 +256,9 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0; virtual HRESULT ObtainNSViewHandle(void** retOut) = 0; virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0; + virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, + IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) = 0; + virtual HRESULT SetBlurEnabled (bool enable) = 0; }; AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase @@ -245,7 +268,8 @@ AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase { - virtual HRESULT ShowDialog (IAvnWindow* parent) = 0; + virtual HRESULT SetEnabled (bool enable) = 0; + virtual HRESULT SetParent (IAvnWindow* parent) = 0; virtual HRESULT SetCanResize(bool value) = 0; virtual HRESULT SetDecorations(SystemDecorations value) = 0; virtual HRESULT SetTitle (void* utf8Title) = 0; @@ -271,6 +295,9 @@ AVNCOM(IAvnWindowBaseEvents, 05) : IUnknown virtual bool RawTextInputEvent (unsigned int timeStamp, const char* text) = 0; virtual void ScalingChanged(double scaling) = 0; virtual void RunRenderPriorityJobs() = 0; + virtual AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, + AvnInputModifiers modifiers, AvnDragDropEffects effects, + IAvnClipboard* clipboard, void* dataObjectHandle) = 0; }; @@ -284,6 +311,8 @@ AVNCOM(IAvnWindowEvents, 06) : IAvnWindowBaseEvents virtual bool Closing () = 0; virtual void WindowStateChanged (AvnWindowState state) = 0; + + virtual void GotInputWhenDisabled () = 0; }; AVNCOM(IAvnMacOptions, 07) : IUnknown @@ -354,8 +383,13 @@ AVNCOM(IAvnScreens, 0e) : IUnknown AVNCOM(IAvnClipboard, 0f) : IUnknown { - virtual HRESULT GetText (IAvnString**ppv) = 0; - virtual HRESULT SetText (void* utf8Text) = 0; + virtual HRESULT GetText (char* type, IAvnString**ppv) = 0; + virtual HRESULT SetText (char* type, void* utf8Text) = 0; + virtual HRESULT ObtainFormats(IAvnStringArray**ppv) = 0; + virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) = 0; + virtual HRESULT SetBytes(char* type, void* utf8Text, int len) = 0; + virtual HRESULT GetBytes(char* type, IAvnString**ppv) = 0; + virtual HRESULT Clear() = 0; }; @@ -428,4 +462,20 @@ AVNCOM(IAvnMenuEvents, 1A) : IUnknown virtual void NeedsUpdate () = 0; }; +AVNCOM(IAvnStringArray, 20) : IUnknown +{ + virtual unsigned int GetCount() = 0; + virtual HRESULT Get(unsigned int index, IAvnString**ppv) = 0; +}; + +AVNCOM(IAvnDndResultCallback, 21) : IUnknown +{ + virtual void OnDragAndDropComplete(AvnDragDropEffects effecct) = 0; +}; + +AVNCOM(IAvnGCHandleDeallocatorCallback, 22) : IUnknown +{ + virtual void FreeGCHandle(void* handle) = 0; +}; + extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative(); diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 50a85bdf9f..ea28780c81 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */; }; 1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */; }; 1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */; }; + 1A465D10246AB61600C5858B /* dnd.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A465D0F246AB61600C5858B /* dnd.mm */; }; 37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; }; 37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; }; 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; }; @@ -33,6 +34,7 @@ 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOSurface.framework; path = System/Library/Frameworks/IOSurface.framework; sourceTree = SDKROOT; }; 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cgl.mm; sourceTree = ""; }; 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + 1A465D0F246AB61600C5858B /* dnd.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = dnd.mm; sourceTree = ""; }; 37155CE3233C00EB0034DCE9 /* menu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = menu.h; sourceTree = ""; }; 379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = ""; }; 37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = ""; }; @@ -92,6 +94,7 @@ 5BF943652167AD1D009CAE35 /* cursor.h */, 5B21A981216530F500CEE36E /* cursor.mm */, 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */, + 1A465D0F246AB61600C5858B /* dnd.mm */, AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */, AB661C212148288600291242 /* common.h */, 379860FE214DA0C000CD0246 /* KeyTransform.h */, @@ -196,6 +199,7 @@ 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */, 520624B322973F4100C4DCEF /* menu.mm in Sources */, 37A517B32159597E00FBA241 /* Screens.mm in Sources */, + 1A465D10246AB61600C5858B /* dnd.mm in Sources */, AB00E4F72147CA920032A60A /* main.mm in Sources */, 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */, AB661C202148286E00291242 /* window.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/AvnString.h b/native/Avalonia.Native/src/OSX/AvnString.h index 9a8f5a1318..5d299374e5 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.h +++ b/native/Avalonia.Native/src/OSX/AvnString.h @@ -10,5 +10,7 @@ #define AvnString_h extern IAvnString* CreateAvnString(NSString* string); - +extern IAvnStringArray* CreateAvnStringArray(NSArray* array); +extern IAvnStringArray* CreateAvnStringArray(NSString* string); +extern IAvnString* CreateByteArray(void* data, int len); #endif /* AvnString_h */ diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index b62fe8a968..00b748ef63 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -7,6 +7,7 @@ // #include "common.h" +#include class AvnStringImpl : public virtual ComSingleObject { @@ -28,6 +29,13 @@ public: memcpy((void*)_cstring, (void*)cstring, _length); } + AvnStringImpl(void*ptr, int len) + { + _length = len; + _cstring = (const char*)malloc(_length); + memcpy((void*)_cstring, ptr, len); + } + virtual ~AvnStringImpl() { free((void*)_cstring); @@ -61,7 +69,60 @@ public: } }; +class AvnStringArrayImpl : public virtual ComSingleObject +{ +private: + std::vector> _list; +public: + FORWARD_IUNKNOWN() + AvnStringArrayImpl(NSArray* array) + { + for(int c = 0; c < [array count]; c++) + { + ComPtr s; + *s.getPPV() = new AvnStringImpl([array objectAtIndex:c]); + _list.push_back(s); + } + } + + AvnStringArrayImpl(NSString* string) + { + ComPtr s; + *s.getPPV() = new AvnStringImpl(string); + _list.push_back(s); + } + + virtual unsigned int GetCount() override + { + return (unsigned int)_list.size(); + } + + virtual HRESULT Get(unsigned int index, IAvnString**ppv) override + { + if(_list.size() <= index) + return E_INVALIDARG; + *ppv = _list[index].getRetainedReference(); + return S_OK; + } +}; + IAvnString* CreateAvnString(NSString* string) { return new AvnStringImpl(string); } + + +IAvnStringArray* CreateAvnStringArray(NSArray * array) +{ + return new AvnStringArrayImpl(array); +} + +IAvnStringArray* CreateAvnStringArray(NSString* string) +{ + return new AvnStringArrayImpl(string); +} + +IAvnString* CreateByteArray(void* data, int len) +{ + return new AvnStringImpl(data, len); +} diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index c2cf1f1f61..116a08670e 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -3,16 +3,27 @@ class Clipboard : public ComSingleObject { +private: + NSPasteboard* _pb; + NSPasteboardItem* _item; public: FORWARD_IUNKNOWN() - Clipboard() + Clipboard(NSPasteboard* pasteboard, NSPasteboardItem* item) { - NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; - [pasteBoard stringForType:NSPasteboardTypeString]; + if(pasteboard == nil && item == nil) + pasteboard = [NSPasteboard generalPasteboard]; + + _pb = pasteboard; + _item = item; } - virtual HRESULT GetText (IAvnString**ppv) override + NSPasteboardItem* TryGetItem() + { + return _item; + } + + virtual HRESULT GetText (char* type, IAvnString**ppv) override { @autoreleasepool { @@ -20,39 +31,124 @@ public: { return E_POINTER; } + NSString* typeString = [NSString stringWithUTF8String:(const char*)type]; + NSString* string = _item == nil ? [_pb stringForType:typeString] : [_item stringForType:typeString]; - *ppv = CreateAvnString([[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]); + *ppv = CreateAvnString(string); return S_OK; } } - virtual HRESULT SetText (void* utf8String) override + virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) override { @autoreleasepool { - NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; - [pasteBoard clearContents]; - [pasteBoard setString:[NSString stringWithUTF8String:(const char*)utf8String] forType:NSPasteboardTypeString]; + *ppv= nil; + NSString* typeString = [NSString stringWithUTF8String:(const char*)type]; + NSObject* data = _item == nil ? [_pb propertyListForType: typeString] : [_item propertyListForType: typeString]; + if(data == nil) + return S_OK; + + if([data isKindOfClass: [NSString class]]) + { + *ppv = CreateAvnStringArray((NSString*) data); + return S_OK; + } + + NSArray* arr = (NSArray*)data; + + for(int c = 0; c < [arr count]; c++) + if(![[arr objectAtIndex:c] isKindOfClass:[NSString class]]) + return E_INVALIDARG; + + *ppv = CreateAvnStringArray(arr); + return S_OK; + } + } + + virtual HRESULT SetText (char* type, void* utf8String) override + { + Clear(); + @autoreleasepool + { + 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; } + + 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; + } + + virtual HRESULT GetBytes(char* type, IAvnString**ppv) override + { + *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; + } + virtual HRESULT Clear() override { @autoreleasepool { - NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; - [pasteBoard clearContents]; - [pasteBoard setString:@"" forType:NSPasteboardTypeString]; + if(_item != nil) + _item = [NSPasteboardItem new]; + else + { + [_pb clearContents]; + [_pb setString:@"" forType:NSPasteboardTypeString]; + } } return S_OK; } + + virtual HRESULT ObtainFormats(IAvnStringArray** ppv) override + { + *ppv = CreateAvnStringArray(_item == nil ? [_pb types] : [_item types]); + return S_OK; + } }; -extern IAvnClipboard* CreateClipboard() +extern IAvnClipboard* CreateClipboard(NSPasteboard* pb, NSPasteboardItem* item) +{ + return new Clipboard(pb, item); +} + +extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*cb) { - return new Clipboard(); + auto clipboard = dynamic_cast(cb); + if(clipboard == nil) + return nil; + return clipboard->TryGetItem(); } diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 7a433bfd9f..df6a7be91c 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -8,11 +8,17 @@ #include extern IAvnPlatformThreadingInterface* CreatePlatformThreading(); +extern void FreeAvnGCHandle(void* handle); extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl); extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl); extern IAvnSystemDialogs* CreateSystemDialogs(); extern IAvnScreens* CreateScreens(); -extern IAvnClipboard* CreateClipboard(); +extern IAvnClipboard* CreateClipboard(NSPasteboard*, NSPasteboardItem*); +extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*); +extern NSObject* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle); +extern void* GetAvnDataObjectHandleFromDraggingInfo(NSObject* info); +extern NSString* GetAvnCustomDataType(); +extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlDisplay* GetGlDisplay(); extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events); diff --git a/native/Avalonia.Native/src/OSX/dnd.mm b/native/Avalonia.Native/src/OSX/dnd.mm new file mode 100644 index 0000000000..294b8ee8ea --- /dev/null +++ b/native/Avalonia.Native/src/OSX/dnd.mm @@ -0,0 +1,89 @@ +#include "common.h" + +extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop) +{ + int effects = 0; + if((nsop & NSDragOperationCopy) != 0) + effects |= (int)AvnDragDropEffects::Copy; + if((nsop & NSDragOperationMove) != 0) + effects |= (int)AvnDragDropEffects::Move; + if((nsop & NSDragOperationLink) != 0) + effects |= (int)AvnDragDropEffects::Link; + return (AvnDragDropEffects)effects; +}; + +extern NSString* GetAvnCustomDataType() +{ + char buffer[256]; + sprintf(buffer, "net.avaloniaui.inproc.uti.n%in", getpid()); + return [NSString stringWithUTF8String:buffer]; +} + +@interface AvnDndSource : NSObject + +@end + +@implementation AvnDndSource +{ + NSDragOperation _operation; + ComPtr _cb; + void* _sourceHandle; +}; + +- (NSDragOperation)draggingSession:(nonnull NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context +{ + return NSDragOperationCopy; +} + +- (AvnDndSource*) initWithOperation: (NSDragOperation)operation + andCallback: (IAvnDndResultCallback*) cb + andSourceHandle: (void*) handle +{ + self = [super init]; + _operation = operation; + _cb = cb; + _sourceHandle = handle; + return self; +} + +- (void)draggingSession:(NSDraggingSession *)session + endedAtPoint:(NSPoint)screenPoint + operation:(NSDragOperation)operation +{ + if(_cb != nil) + { + auto cb = _cb; + _cb = nil; + cb->OnDragAndDropComplete(ConvertDragDropEffects(operation)); + } + if(_sourceHandle != nil) + { + FreeAvnGCHandle(_sourceHandle); + _sourceHandle = nil; + } +} + +- (void*) gcHandle +{ + return _sourceHandle; +} + +@end + +extern NSObject* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle) +{ + return [[AvnDndSource alloc] initWithOperation:op andCallback:cb andSourceHandle:handle]; +}; + +extern void* GetAvnDataObjectHandleFromDraggingInfo(NSObject* info) +{ + id obj = [info draggingSource]; + if(obj == nil) + return nil; + if([obj isKindOfClass: [AvnDndSource class]]) + { + auto src = (AvnDndSource*)obj; + return [src gcHandle]; + } + return nil; +} diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index a63353bc0a..e6c4a861fd 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -150,14 +150,15 @@ public: } @end - +static ComPtr _deallocator; class AvaloniaNative : public ComSingleObject { public: FORWARD_IUNKNOWN() - virtual HRESULT Initialize() override + virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) override { + _deallocator = deallocator; @autoreleasepool{ [[ThreadingInitializer new] do]; } @@ -207,7 +208,13 @@ public: virtual HRESULT CreateClipboard(IAvnClipboard** ppv) override { - *ppv = ::CreateClipboard (); + *ppv = ::CreateClipboard (nil, nil); + return S_OK; + } + + virtual HRESULT CreateDndClipboard(IAvnClipboard** ppv) override + { + *ppv = ::CreateClipboard (nil, [NSPasteboardItem new]); return S_OK; } @@ -257,6 +264,12 @@ extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative() return new AvaloniaNative(); }; +extern void FreeAvnGCHandle(void* handle) +{ + if(_deallocator != nil) + _deallocator->FreeGCHandle(handle); +} + NSSize ToNSSize (AvnSize s) { NSSize result; diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index ec8fe9e6ee..bdf3007a28 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -3,7 +3,10 @@ class WindowBaseImpl; -@interface AvnView : NSView +@interface AutoFitContentVisualEffectView : NSVisualEffectView +@end + +@interface AvnView : NSView -(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(NSEvent* _Nonnull) lastMouseDownEvent; -(AvnPoint) translateLocalPoint:(AvnPoint)pt; @@ -19,8 +22,7 @@ class WindowBaseImpl; -(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) restoreParentWindow; -(bool) shouldTryToHandleEvents; --(bool) isModal; --(void) setModal: (bool) isModal; +-(void) setEnabled: (bool) enable; -(void) showAppMenuOnly; -(void) showWindowMenuWithAppMenu; -(void) applyMenu:(NSMenu* _Nullable)menu; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 091219fc72..abfae3cf1e 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -20,6 +20,7 @@ public: View = NULL; Window = NULL; } + NSVisualEffectView* VisualEffect; AvnView* View; AvnWindow* Window; ComPtr BaseEvents; @@ -47,6 +48,12 @@ public: [Window setStyleMask:NSWindowStyleMaskBorderless]; [Window setBackingType:NSBackingStoreBuffered]; + + VisualEffect = [AutoFitContentVisualEffectView new]; + [VisualEffect setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; + [VisualEffect setMaterial:NSVisualEffectMaterialLight]; + [VisualEffect setAutoresizesSubviews:true]; + [Window setContentView: View]; } @@ -382,6 +389,62 @@ public: *ppv = [renderTarget createSurfaceRenderTarget]; return *ppv == nil ? E_FAIL : S_OK; } + + virtual HRESULT SetBlurEnabled (bool enable) override + { + [Window setContentView: enable ? VisualEffect : View]; + + if(enable) + { + [VisualEffect addSubview:View]; + } + + return S_OK; + } + + virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, + IAvnClipboard* clipboard, IAvnDndResultCallback* cb, + void* sourceHandle) override + { + auto item = TryGetPasteboardItem(clipboard); + [item setString:@"" forType:GetAvnCustomDataType()]; + if(item == nil) + return E_INVALIDARG; + if(View == NULL) + return E_FAIL; + + auto nsevent = [NSApp currentEvent]; + auto nseventType = [nsevent type]; + + // If current event isn't a mouse one (probably due to malfunctioning user app) + // attempt to forge a new one + if(!((nseventType >= NSEventTypeLeftMouseDown && nseventType <= NSEventTypeMouseExited) + || (nseventType >= NSEventTypeOtherMouseDown && nseventType <= NSEventTypeOtherMouseDragged))) + { + auto nspoint = [Window convertBaseToScreen: ToNSPoint(point)]; + CGPoint cgpoint = NSPointToCGPoint(nspoint); + auto cgevent = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, cgpoint, kCGMouseButtonLeft); + nsevent = [NSEvent eventWithCGEvent: cgevent]; + CFRelease(cgevent); + } + + auto dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter: item]; + + auto dragItemImage = [NSImage imageNamed:NSImageNameMultipleDocuments]; + NSRect dragItemRect = {(float)point.X, (float)point.Y, [dragItemImage size].width, [dragItemImage size].height}; + [dragItem setDraggingFrame: dragItemRect contents: dragItemImage]; + + int op = 0; int ieffects = (int)effects; + if((ieffects & (int)AvnDragDropEffects::Copy) != 0) + op |= NSDragOperationCopy; + if((ieffects & (int)AvnDragDropEffects::Link) != 0) + op |= NSDragOperationLink; + if((ieffects & (int)AvnDragDropEffects::Move) != 0) + op |= NSDragOperationMove; + [View beginDraggingSessionWithItems: @[dragItem] event: nsevent + source: CreateDraggingSource((NSDragOperation) op, cb, sourceHandle)]; + return S_OK; + } protected: virtual NSWindowStyleMask GetStyle() @@ -453,12 +516,7 @@ private: virtual HRESULT Show () override { @autoreleasepool - { - if([Window parentWindow] != nil) - [[Window parentWindow] removeChildWindow:Window]; - - [Window setModal:FALSE]; - + { WindowBaseImpl::Show(); HideOrShowTrafficLights(); @@ -467,7 +525,16 @@ private: } } - virtual HRESULT ShowDialog (IAvnWindow* parent) override + virtual HRESULT SetEnabled (bool enable) override + { + @autoreleasepool + { + [Window setEnabled:enable]; + return S_OK; + } + } + + virtual HRESULT SetParent (IAvnWindow* parent) override { @autoreleasepool { @@ -478,12 +545,9 @@ private: if(cparent == nullptr) return E_INVALIDARG; - [Window setModal:TRUE]; - [cparent->Window addChildWindow:Window ordered:NSWindowAbove]; - WindowBaseImpl::Show(); - HideOrShowTrafficLights(); + UpdateStyle(); return S_OK; } @@ -839,15 +903,15 @@ protected: switch (_decorations) { case SystemDecorationsNone: - s = s | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable; + s = s | NSWindowStyleMaskFullSizeContentView; break; case SystemDecorationsBorderOnly: - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable; + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; break; case SystemDecorationsFull: - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless; + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskBorderless; if(_canResize) { @@ -856,12 +920,26 @@ protected: break; } + if([Window parentWindow] == nullptr) + { + s |= NSWindowStyleMaskMiniaturizable; + } return s; } }; NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil]; +@implementation AutoFitContentVisualEffectView +-(void)setFrameSize:(NSSize)newSize +{ + [super setFrameSize:newSize]; + if([[self subviews] count] == 0) + return; + [[self subviews][0] setFrameSize: newSize]; +} +@end + @implementation AvnView { ComPtr _parent; @@ -911,7 +989,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _area = nullptr; _lastPixelSize.Height = 100; _lastPixelSize.Width = 100; - + [self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]]; return self; } @@ -1037,15 +1115,28 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (bool) ignoreUserInput { auto parentWindow = objc_cast([self window]); + if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents]) + { + auto window = dynamic_cast(_parent.getRaw()); + + if(window != nullptr) + { + window->WindowEvents->GotInputWhenDisabled(); + } + return TRUE; + } + return FALSE; } - (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type { if([self ignoreUserInput]) + { return; + } [self becomeFirstResponder]; auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; @@ -1190,7 +1281,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type { if([self ignoreUserInput]) + { return; + } + auto key = s_KeyMap[[event keyCode]]; auto timestamp = [event timestamp] * 1000; @@ -1302,6 +1396,68 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return result; } + +- (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id )info +{ + auto localPoint = [self convertPoint:[info draggingLocation] toView:self]; + auto avnPoint = [self toAvnPoint:localPoint]; + auto point = [self translateLocalPoint:avnPoint]; + auto modifiers = [self getModifiers:[[NSApp currentEvent] modifierFlags]]; + NSDragOperation nsop = [info draggingSourceOperationMask]; + + auto effects = ConvertDragDropEffects(nsop); + int reffects = (int)_parent->BaseEvents + ->DragEvent(type, point, modifiers, effects, + CreateClipboard([info draggingPasteboard], nil), + GetAvnDataObjectHandleFromDraggingInfo(info)); + + NSDragOperation ret = 0; + + // Ensure that the managed part didn't add any new effects + reffects = (int)effects & (int)reffects; + + // OSX requires exactly one operation + if((reffects & (int)AvnDragDropEffects::Copy) != 0) + ret = NSDragOperationCopy; + else if((reffects & (int)AvnDragDropEffects::Move) != 0) + ret = NSDragOperationMove; + else if((reffects & (int)AvnDragDropEffects::Link) != 0) + ret = NSDragOperationLink; + if(ret == 0) + ret = NSDragOperationNone; + return ret; +} + +- (NSDragOperation)draggingEntered:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Enter info:sender]; +} + +- (NSDragOperation)draggingUpdated:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender]; +} + +- (void)draggingExited:(id )sender +{ + [self triggerAvnDragEvent: AvnDragEventType::Leave info:sender]; +} + +- (BOOL)prepareForDragOperation:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender] != NSDragOperationNone; +} + +- (BOOL)performDragOperation:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Drop info:sender] != NSDragOperationNone; +} + +- (void)concludeDragOperation:(nullable id )sender +{ + +} + @end @@ -1310,7 +1466,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent ComPtr _parent; bool _canBecomeKeyAndMain; bool _closed; - bool _isModal; + bool _isEnabled; AvnMenu* _menu; double _lastScaling; } @@ -1432,6 +1588,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _parent = parent; [self setDelegate:self]; _closed = false; + _isEnabled = true; _lastScaling = [self backingScaleFactor]; [self setOpaque:NO]; @@ -1498,28 +1655,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent -(bool)shouldTryToHandleEvents { - for(NSWindow* uch in [self childWindows]) - { - auto ch = objc_cast(uch); - if(ch == nil) - continue; - - if(![ch isModal]) - continue; - - return FALSE; - } - return TRUE; -} - --(bool) isModal -{ - return _isModal; + return _isEnabled; } --(void) setModal: (bool) isModal +-(void) setEnabled:(bool)enable { - _isModal = isModal; + _isEnabled = enable; } -(void)makeKeyWindow diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 552713f94b..537495fcad 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -2,6 +2,7 @@ <_AvaloniaUseExternalMSBuild>$(AvaloniaUseExternalMSBuild) <_AvaloniaUseExternalMSBuild Condition="'$(_AvaloniaForceInternalMSBuild)' == 'true'">false + low + EmbeddedResources="@(EmbeddedResources)" + ReportImportance="$(AvaloniaXamlReportImportance)"/> @@ -67,6 +69,7 @@ OriginalCopyPath="$(AvaloniaXamlOriginalCopyFilePath)" ProjectDirectory="$(MSBuildProjectDirectory)" VerifyIl="$(AvaloniaXamlIlVerifyIl)" + ReportImportance="$(AvaloniaXamlReportImportance)" /> - - - + - + + + + diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml.cs b/samples/ControlCatalog/Pages/ButtonPage.xaml.cs index 1d0c228a0e..5e555c8c91 100644 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ButtonPage.xaml.cs @@ -5,5 +5,25 @@ namespace ControlCatalog.Pages { public class ButtonPage : UserControl { + private int repeatButtonClickCount = 0; + + public ButtonPage() + { + InitializeComponent(); + + this.FindControl("RepeatButton").Click += OnRepeatButtonClick; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public void OnRepeatButtonClick(object sender, object args) + { + repeatButtonClickCount++; + var textBlock = this.FindControl("RepeatButtonTextBlock"); + textBlock.Text = $"Repeat Button: {repeatButtonClickCount}"; + } } } diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml b/samples/ControlCatalog/Pages/ComboBoxPage.xaml index bbfbd4b4cd..486cc55d44 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml @@ -6,7 +6,7 @@ A drop-down list. - + Inline Items Inline Item 2 Inline Item 3 diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml index 9bfcd90149..65a798e53c 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml @@ -9,9 +9,15 @@ Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16"> - - Drag Me - + + + Drag Me + + + Drag Me (custom) + + + Drop some text or files here diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs index 0bf21c2820..5a52dbe12b 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs @@ -3,69 +3,85 @@ using Avalonia.Input; using Avalonia.Markup.Xaml; using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; using System.Text; namespace ControlCatalog.Pages { public class DragAndDropPage : UserControl { - private TextBlock _DropState; - private TextBlock _DragState; - private Border _DragMe; - private int DragCount = 0; - + TextBlock _DropState; + private const string CustomFormat = "application/xxx-avalonia-controlcatalog-custom"; public DragAndDropPage() { this.InitializeComponent(); + _DropState = this.Find("DropState"); - _DragMe.PointerPressed += DoDrag; + int textCount = 0; + SetupDnd("Text", d => d.Set(DataFormats.Text, + $"Text was dragged {++textCount} times")); - AddHandler(DragDrop.DropEvent, Drop); - AddHandler(DragDrop.DragOverEvent, DragOver); + SetupDnd("Custom", d => d.Set(CustomFormat, "Test123")); } - private async void DoDrag(object sender, Avalonia.Input.PointerPressedEventArgs e) + void SetupDnd(string suffix, Action factory, DragDropEffects effects = DragDropEffects.Copy) { - DataObject dragData = new DataObject(); - dragData.Set(DataFormats.Text, $"You have dragged text {++DragCount} times"); + var dragMe = this.Find("DragMe" + suffix); + var dragState = this.Find("DragState"+suffix); - var result = await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Copy); - switch(result) + async void DoDrag(object sender, Avalonia.Input.PointerPressedEventArgs e) { - case DragDropEffects.Copy: - _DragState.Text = "The text was copied"; break; - case DragDropEffects.Link: - _DragState.Text = "The text was linked"; break; - case DragDropEffects.None: - _DragState.Text = "The drag operation was canceled"; break; + var dragData = new DataObject(); + factory(dragData); + + var result = await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Copy); + switch (result) + { + case DragDropEffects.Copy: + dragState.Text = "Data was copied"; + break; + case DragDropEffects.Link: + dragState.Text = "Data was linked"; + break; + case DragDropEffects.None: + dragState.Text = "The drag operation was canceled"; + break; + } } - } - private void DragOver(object sender, DragEventArgs e) - { - // Only allow Copy or Link as Drop Operations. - e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link); + void DragOver(object sender, DragEventArgs e) + { + // Only allow Copy or Link as Drop Operations. + e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link); - // Only allow if the dragged data contains text or filenames. - if (!e.Data.Contains(DataFormats.Text) && !e.Data.Contains(DataFormats.FileNames)) - e.DragEffects = DragDropEffects.None; - } + // Only allow if the dragged data contains text or filenames. + if (!e.Data.Contains(DataFormats.Text) + && !e.Data.Contains(DataFormats.FileNames) + && !e.Data.Contains(CustomFormat)) + e.DragEffects = DragDropEffects.None; + } - private void Drop(object sender, DragEventArgs e) - { - if (e.Data.Contains(DataFormats.Text)) - _DropState.Text = e.Data.GetText(); - else if (e.Data.Contains(DataFormats.FileNames)) - _DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames()); + void Drop(object sender, DragEventArgs e) + { + if (e.Data.Contains(DataFormats.Text)) + _DropState.Text = e.Data.GetText(); + else if (e.Data.Contains(DataFormats.FileNames)) + _DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames()); + else if (e.Data.Contains(CustomFormat)) + _DropState.Text = "Custom: " + e.Data.Get(CustomFormat); + } + + dragMe.PointerPressed += DoDrag; + + AddHandler(DragDrop.DropEvent, Drop); + AddHandler(DragDrop.DragOverEvent, DragOver); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); - - _DropState = this.Find("DropState"); - _DragState = this.Find("DragState"); - _DragMe = this.Find("DragMe"); } } } diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml index 58f7b881fe..c6f5521e60 100644 --- a/samples/ControlCatalog/Pages/SliderPage.xaml +++ b/samples/ControlCatalog/Pages/SliderPage.xaml @@ -9,12 +9,14 @@ diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index c9e3fafb6d..789b45e62c 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -19,8 +19,8 @@ - + Single diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs index 5893796b8b..3fb990459f 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs @@ -1,9 +1,6 @@ -using System.Collections.ObjectModel; -using System.Linq; -using System.Reactive; using Avalonia.Controls; using Avalonia.Markup.Xaml; -using ReactiveUI; +using ControlCatalog.ViewModels; namespace ControlCatalog.Pages { @@ -12,105 +9,12 @@ namespace ControlCatalog.Pages public TreeViewPage() { InitializeComponent(); - DataContext = new PageViewModel(); + DataContext = new TreeViewPageViewModel(); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } - - private class PageViewModel : ReactiveObject - { - private SelectionMode _selectionMode; - - public PageViewModel() - { - Node root = new Node(); - Items = root.Children; - Selection = new SelectionModel(); - - AddItemCommand = ReactiveCommand.Create(() => - { - Node parentItem = Selection.SelectedItems.Count > 0 ? - (Node)Selection.SelectedItems[0] : root; - parentItem.AddNewItem(); - }); - - RemoveItemCommand = ReactiveCommand.Create(() => - { - while (Selection.SelectedItems.Count > 0) - { - Node lastItem = (Node)Selection.SelectedItems[0]; - RecursiveRemove(Items, lastItem); - Selection.DeselectAt(Selection.SelectedIndices[0]); - } - - bool RecursiveRemove(ObservableCollection items, Node selectedItem) - { - if (items.Remove(selectedItem)) - { - return true; - } - - foreach (Node item in items) - { - if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem)) - { - return true; - } - } - - return false; - } - }); - } - - public ObservableCollection Items { get; } - - public SelectionModel Selection { get; } - - public ReactiveCommand AddItemCommand { get; } - - public ReactiveCommand RemoveItemCommand { get; } - - public SelectionMode SelectionMode - { - get => _selectionMode; - set - { - Selection.ClearSelection(); - this.RaiseAndSetIfChanged(ref _selectionMode, value); - } - } - } - - private class Node - { - private int _counter; - private ObservableCollection _children; - - public string Header { get; private set; } - - public bool AreChildrenInitialized => _children != null; - - public ObservableCollection Children - { - get - { - if (_children == null) - { - _children = new ObservableCollection(Enumerable.Range(1, 10).Select(i => CreateNewNode())); - } - return _children; - } - } - - public void AddNewItem() => Children.Add(CreateNewNode()); - - public override string ToString() => Header; - - private Node CreateNewNode() => new Node { Header = $"Item {_counter++}" }; - } } } diff --git a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs new file mode 100644 index 0000000000..5bc23e2fe5 --- /dev/null +++ b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using Avalonia.Controls; +using ReactiveUI; + +namespace ControlCatalog.ViewModels +{ + public class TreeViewPageViewModel : ReactiveObject + { + private readonly Node _root; + private SelectionMode _selectionMode; + + public TreeViewPageViewModel() + { + _root = new Node(); + + Items = _root.Children; + Selection = new SelectionModel(); + Selection.SelectionChanged += SelectionChanged; + + AddItemCommand = ReactiveCommand.Create(AddItem); + RemoveItemCommand = ReactiveCommand.Create(RemoveItem); + SelectRandomItemCommand = ReactiveCommand.Create(SelectRandomItem); + } + + public ObservableCollection Items { get; } + public SelectionModel Selection { get; } + public ReactiveCommand AddItemCommand { get; } + public ReactiveCommand RemoveItemCommand { get; } + public ReactiveCommand SelectRandomItemCommand { get; } + + public SelectionMode SelectionMode + { + get => _selectionMode; + set + { + Selection.ClearSelection(); + this.RaiseAndSetIfChanged(ref _selectionMode, value); + } + } + + private void AddItem() + { + var parentItem = Selection.SelectedItems.Count > 0 ? (Node)Selection.SelectedItems[0] : _root; + parentItem.AddItem(); + } + + private void RemoveItem() + { + while (Selection.SelectedItems.Count > 0) + { + Node lastItem = (Node)Selection.SelectedItems[0]; + RecursiveRemove(Items, lastItem); + Selection.DeselectAt(Selection.SelectedIndices[0]); + } + + bool RecursiveRemove(ObservableCollection items, Node selectedItem) + { + if (items.Remove(selectedItem)) + { + return true; + } + + foreach (Node item in items) + { + if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem)) + { + return true; + } + } + + return false; + } + } + + private void SelectRandomItem() + { + var random = new Random(); + var depth = random.Next(4); + var indexes = Enumerable.Range(0, 4).Select(x => random.Next(10)); + var path = new IndexPath(indexes); + Selection.SelectedIndex = path; + } + + private void SelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) + { + var selected = string.Join(",", e.SelectedIndices); + var deselected = string.Join(",", e.DeselectedIndices); + System.Diagnostics.Debug.WriteLine($"Selected '{selected}', Deselected '{deselected}'"); + } + + public class Node + { + private ObservableCollection _children; + private int _childIndex = 10; + + public Node() + { + Header = "Item"; + } + + public Node(Node parent, int index) + { + Parent = parent; + Header = parent.Header + ' ' + index; + } + + public Node Parent { get; } + public string Header { get; } + public bool AreChildrenInitialized => _children != null; + public ObservableCollection Children => _children ??= CreateChildren(); + public void AddItem() => Children.Add(new Node(this, _childIndex++)); + public void RemoveItem(Node child) => Children.Remove(child); + public override string ToString() => Header; + + private ObservableCollection CreateChildren() + { + return new ObservableCollection( + Enumerable.Range(0, 10).Select(i => new Node(this, i))); + } + } + } +} diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props index b9075b957b..8fc91aca14 100644 --- a/samples/Directory.Build.props +++ b/samples/Directory.Build.props @@ -1,6 +1,7 @@ false + $(MSBuildThisFileDirectory)..\src\tools\Avalonia.Designer.HostApp\bin\Debug\netcoreapp2.0\Avalonia.Designer.HostApp.dll diff --git a/samples/RenderDemo/Controls/LineBoundsDemoControl.cs b/samples/RenderDemo/Controls/LineBoundsDemoControl.cs new file mode 100644 index 0000000000..cc847a594d --- /dev/null +++ b/samples/RenderDemo/Controls/LineBoundsDemoControl.cs @@ -0,0 +1,53 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Threading; + +namespace RenderDemo.Controls +{ + public class LineBoundsDemoControl : Control + { + static LineBoundsDemoControl() + { + AffectsRender(AngleProperty); + } + + public LineBoundsDemoControl() + { + var timer = new DispatcherTimer(); + timer.Interval = TimeSpan.FromSeconds(1 / 60.0); + timer.Tick += (sender, e) => Angle += Math.PI / 360; + timer.Start(); + } + + public static readonly StyledProperty AngleProperty = + AvaloniaProperty.Register(nameof(Angle)); + + public double Angle + { + get => GetValue(AngleProperty); + set => SetValue(AngleProperty, value); + } + + public override void Render(DrawingContext drawingContext) + { + var lineLength = Math.Sqrt((100 * 100) + (100 * 100)); + + var diffX = LineBoundsHelper.CalculateAdjSide(Angle, lineLength); + var diffY = LineBoundsHelper.CalculateOppSide(Angle, lineLength); + + + var p1 = new Point(200, 200); + var p2 = new Point(p1.X + diffX, p1.Y + diffY); + + var pen = new Pen(Brushes.Green, 20, lineCap: PenLineCap.Square); + var boundPen = new Pen(Brushes.Black); + + drawingContext.DrawLine(pen, p1, p2); + + drawingContext.DrawRectangle(boundPen, LineBoundsHelper.CalculateBounds(p1, p2, pen)); + } + } +} diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index b17520a466..c098ef411e 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -44,6 +44,9 @@ + + + diff --git a/samples/RenderDemo/Pages/LineBoundsPage.xaml b/samples/RenderDemo/Pages/LineBoundsPage.xaml new file mode 100644 index 0000000000..07d658630a --- /dev/null +++ b/samples/RenderDemo/Pages/LineBoundsPage.xaml @@ -0,0 +1,9 @@ + + + diff --git a/samples/RenderDemo/Pages/LineBoundsPage.xaml.cs b/samples/RenderDemo/Pages/LineBoundsPage.xaml.cs new file mode 100644 index 0000000000..28ddedd4bc --- /dev/null +++ b/samples/RenderDemo/Pages/LineBoundsPage.xaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace RenderDemo.Pages +{ + public class LineBoundsPage : UserControl + { + public LineBoundsPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/RenderDemo/RenderDemo.csproj b/samples/RenderDemo/RenderDemo.csproj index ce33f42143..0d7d62e177 100644 --- a/samples/RenderDemo/RenderDemo.csproj +++ b/samples/RenderDemo/RenderDemo.csproj @@ -3,6 +3,9 @@ Exe netcoreapp3.1 + + + diff --git a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs index 51e0a1e799..7802f336fb 100644 --- a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs +++ b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs @@ -43,5 +43,11 @@ namespace Avalonia.Android.Platform return Task.FromResult(null); } + + public Task SetDataObjectAsync(IDataObject data) => throw new PlatformNotSupportedException(); + + public Task GetFormatsAsync() => throw new PlatformNotSupportedException(); + + public Task GetDataAsync(string format) => throw new PlatformNotSupportedException(); } } diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index fa1e955153..067d9f462f 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -1,10 +1,10 @@ using System; +using System.Collections; using System.Collections.Generic; -using System.Linq; -using System.Reactive.Linq; -using Avalonia.Collections; +using System.Collections.Specialized; using Avalonia.Data; -using Avalonia.Animation.Animators; + +#nullable enable namespace Avalonia.Animation { @@ -13,9 +13,24 @@ namespace Avalonia.Animation /// public class Animatable : AvaloniaObject { + /// + /// Defines the property. + /// public static readonly StyledProperty ClockProperty = AvaloniaProperty.Register(nameof(Clock), inherits: true); + /// + /// Defines the property. + /// + public static readonly StyledProperty TransitionsProperty = + AvaloniaProperty.Register(nameof(Transitions)); + + private bool _transitionsEnabled = true; + private Dictionary? _transitionState; + + /// + /// Gets or sets the clock which controls the animations on the control. + /// public IClock Clock { get => GetValue(ClockProperty); @@ -23,72 +38,196 @@ namespace Avalonia.Animation } /// - /// Defines the property. + /// Gets or sets the property transitions for the control. /// - public static readonly DirectProperty TransitionsProperty = - AvaloniaProperty.RegisterDirect( - nameof(Transitions), - o => o.Transitions, - (o, v) => o.Transitions = v); + public Transitions? Transitions + { + get => GetValue(TransitionsProperty); + set => SetValue(TransitionsProperty, value); + } - private Transitions _transitions; + /// + /// Enables transitions for the control. + /// + /// + /// This method should not be called from user code, it will be called automatically by the framework + /// when a control is added to the visual tree. + /// + protected void EnableTransitions() + { + if (!_transitionsEnabled) + { + _transitionsEnabled = true; - private Dictionary _previousTransitions; + if (Transitions is object) + { + AddTransitions(Transitions); + } + } + } /// - /// Gets or sets the property transitions for the control. + /// Disables transitions for the control. /// - public Transitions Transitions + /// + /// This method should not be called from user code, it will be called automatically by the framework + /// when a control is added to the visual tree. + /// + protected void DisableTransitions() + { + if (_transitionsEnabled) + { + _transitionsEnabled = false; + + if (Transitions is object) + { + RemoveTransitions(Transitions); + } + } + } + + protected sealed override void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) { - get + if (change.Property == TransitionsProperty && change.IsEffectiveValueChange) { - if (_transitions is null) - _transitions = new Transitions(); + var oldTransitions = change.OldValue.GetValueOrDefault(); + var newTransitions = change.NewValue.GetValueOrDefault(); - if (_previousTransitions is null) - _previousTransitions = new Dictionary(); + if (newTransitions is object) + { + newTransitions.CollectionChanged += TransitionsCollectionChanged; + AddTransitions(newTransitions); + } - return _transitions; + if (oldTransitions is object) + { + oldTransitions.CollectionChanged -= TransitionsCollectionChanged; + RemoveTransitions(oldTransitions); + } } - set + else if (_transitionsEnabled && + Transitions is object && + _transitionState is object && + !change.Property.IsDirect && + change.Priority > BindingPriority.Animation) { - if (value is null) - return; + for (var i = Transitions.Count -1; i >= 0; --i) + { + var transition = Transitions[i]; + + if (transition.Property == change.Property) + { + var state = _transitionState[transition]; + var oldValue = state.BaseValue; + var newValue = GetAnimationBaseValue(transition.Property); - if (_previousTransitions is null) - _previousTransitions = new Dictionary(); + if (!Equals(oldValue, newValue)) + { + state.BaseValue = newValue; - SetAndRaise(TransitionsProperty, ref _transitions, value); + // We need to transition from the current animated value if present, + // instead of the old base value. + var animatedValue = GetValue(transition.Property); + + if (!Equals(newValue, animatedValue)) + { + oldValue = animatedValue; + } + + state.Instance?.Dispose(); + state.Instance = transition.Apply( + this, + Clock ?? AvaloniaLocator.Current.GetService(), + oldValue, + newValue); + return; + } + } + } + } + + base.OnPropertyChangedCore(change); + } + + private void TransitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (!_transitionsEnabled) + { + return; + } + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + AddTransitions(e.NewItems); + break; + case NotifyCollectionChangedAction.Remove: + RemoveTransitions(e.OldItems); + break; + case NotifyCollectionChangedAction.Replace: + RemoveTransitions(e.OldItems); + AddTransitions(e.NewItems); + break; + case NotifyCollectionChangedAction.Reset: + throw new NotSupportedException("Transitions collection cannot be reset."); } } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + private void AddTransitions(IList items) { - if (_transitions is null || _previousTransitions is null || priority == BindingPriority.Animation) + if (!_transitionsEnabled) + { return; + } + + _transitionState ??= new Dictionary(); - // PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations). - foreach (var transition in _transitions) + for (var i = 0; i < items.Count; ++i) { - if (transition.Property == property) + var t = (ITransition)items[i]; + + _transitionState.Add(t, new TransitionState { - if (_previousTransitions.TryGetValue(property, out var dispose)) - dispose.Dispose(); + BaseValue = GetAnimationBaseValue(t.Property), + }); + } + } - var instance = transition.Apply( - this, - Clock ?? Avalonia.Animation.Clock.GlobalClock, - oldValue.GetValueOrDefault(), - newValue.GetValueOrDefault()); + private void RemoveTransitions(IList items) + { + if (_transitionState is null) + { + return; + } - _previousTransitions[property] = instance; - return; + for (var i = 0; i < items.Count; ++i) + { + var t = (ITransition)items[i]; + + if (_transitionState.TryGetValue(t, out var state)) + { + state.Instance?.Dispose(); + _transitionState.Remove(t); } } } + + private object GetAnimationBaseValue(AvaloniaProperty property) + { + var value = this.GetBaseValue(property, BindingPriority.LocalValue); + + if (value == AvaloniaProperty.UnsetValue) + { + value = GetValue(property); + } + + return value; + } + + private class TransitionState + { + public IDisposable? Instance { get; set; } + public object? BaseValue { get; set; } + } } } diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index efbbed51b5..ad2001d621 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/src/Avalonia.Animation/TransitionInstance.cs @@ -19,6 +19,8 @@ namespace Avalonia.Animation public TransitionInstance(IClock clock, TimeSpan Duration) { + clock = clock ?? throw new ArgumentNullException(nameof(clock)); + _duration = Duration; _baseClock = clock; } diff --git a/src/Avalonia.Animation/Transitions.cs b/src/Avalonia.Animation/Transitions.cs index 2741039ebc..6687a2902d 100644 --- a/src/Avalonia.Animation/Transitions.cs +++ b/src/Avalonia.Animation/Transitions.cs @@ -1,4 +1,6 @@ +using System; using Avalonia.Collections; +using Avalonia.Threading; namespace Avalonia.Animation { @@ -13,6 +15,17 @@ namespace Avalonia.Animation public Transitions() { ResetBehavior = ResetBehavior.Remove; + Validate = ValidateTransition; + } + + private void ValidateTransition(ITransition obj) + { + Dispatcher.UIThread.VerifyAccess(); + + if (obj.Property.IsDirect) + { + throw new InvalidOperationException("Cannot animate a direct property."); + } } } } diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index ed36e6da43..f387d7e0b6 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -259,6 +259,21 @@ namespace Avalonia return registered.InvokeGetter(this); } + /// + public Optional GetBaseValue(StyledPropertyBase property, BindingPriority maxPriority) + { + property = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); + + if (_values is object && + _values.TryGetValue(property, maxPriority, out var value)) + { + return value; + } + + return default; + } + /// /// Checks whether a is animating. /// @@ -458,29 +473,43 @@ namespace Avalonia return _propertyChanged?.GetInvocationList(); } - void IValueSink.ValueChanged( - StyledPropertyBase property, - BindingPriority priority, - Optional oldValue, - BindingValue newValue) + void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change) { - oldValue = oldValue.HasValue ? oldValue : GetInheritedOrDefault(property); - newValue = newValue.HasValue ? newValue : newValue.WithValue(GetInheritedOrDefault(property)); + var property = (StyledPropertyBase)change.Property; - LogIfError(property, newValue); + LogIfError(property, change.NewValue); - if (!EqualityComparer.Default.Equals(oldValue.Value, newValue.Value)) + // If the change is to the effective value of the property and no old/new value is set + // then fill in the old/new value from property inheritance/default value. We don't do + // this for non-effective value changes because these are only needed for property + // transitions, where knowing e.g. that an inherited value is active at an arbitrary + // priority isn't of any use and would introduce overhead. + if (change.IsEffectiveValueChange && !change.OldValue.HasValue) { - RaisePropertyChanged(property, oldValue, newValue, priority); + change.SetOldValue(GetInheritedOrDefault(property)); + } - Logger.TryGet(LogEventLevel.Verbose)?.Log( - LogArea.Property, - this, - "{Property} changed from {$Old} to {$Value} with priority {Priority}", - property, - oldValue, - newValue, - (BindingPriority)priority); + if (change.IsEffectiveValueChange && !change.NewValue.HasValue) + { + change.SetNewValue(GetInheritedOrDefault(property)); + } + + if (!change.IsEffectiveValueChange || + !EqualityComparer.Default.Equals(change.OldValue.Value, change.NewValue.Value)) + { + RaisePropertyChanged(change); + + if (change.IsEffectiveValueChange) + { + Logger.TryGet(LogEventLevel.Verbose)?.Log( + LogArea.Property, + this, + "{Property} changed from {$Old} to {$Value} with priority {Priority}", + property, + change.OldValue, + change.NewValue, + change.Priority); + } } } @@ -489,7 +518,13 @@ namespace Avalonia IPriorityValueEntry entry, Optional oldValue) { - ((IValueSink)this).ValueChanged(property, BindingPriority.Unset, oldValue, default); + var change = new AvaloniaPropertyChangedEventArgs( + this, + property, + oldValue, + default, + BindingPriority.Unset); + ((IValueSink)this).ValueChanged(change); } /// @@ -575,15 +610,20 @@ namespace Avalonia /// /// Called when a avalonia property changes on the object. /// - /// The property whose value has changed. - /// The old value of the property. - /// The new value of the property. - /// The priority of the new value. - protected virtual void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + /// The property change details. + protected virtual void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) + { + if (change.IsEffectiveValueChange) + { + OnPropertyChanged(change); + } + } + + /// + /// Called when a avalonia property changes on the object. + /// + /// The property change details. + protected virtual void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { } @@ -600,57 +640,12 @@ namespace Avalonia BindingValue newValue, BindingPriority priority = BindingPriority.LocalValue) { - property = property ?? throw new ArgumentNullException(nameof(property)); - - VerifyAccess(); - - property.Notifying?.Invoke(this, true); - - try - { - AvaloniaPropertyChangedEventArgs e = null; - var hasChanged = property.HasChangedSubscriptions; - - if (hasChanged || _propertyChanged != null) - { - e = new AvaloniaPropertyChangedEventArgs( - this, - property, - oldValue, - newValue, - priority); - } - - OnPropertyChanged(property, oldValue, newValue, priority); - - if (hasChanged) - { - property.NotifyChanged(e); - } - - _propertyChanged?.Invoke(this, e); - - if (_inpcChanged != null) - { - var inpce = new PropertyChangedEventArgs(property.Name); - _inpcChanged(this, inpce); - } - - if (property.Inherits && _inheritanceChildren != null) - { - foreach (var child in _inheritanceChildren) - { - child.InheritedPropertyChanged( - property, - oldValue, - newValue.ToOptional()); - } - } - } - finally - { - property.Notifying?.Invoke(this, false); - } + RaisePropertyChanged(new AvaloniaPropertyChangedEventArgs( + this, + property, + oldValue, + newValue, + priority)); } /// @@ -689,7 +684,9 @@ namespace Avalonia return property.GetDefaultValue(GetType()); } - private T GetValueOrInheritedOrDefault(StyledPropertyBase property) + private T GetValueOrInheritedOrDefault( + StyledPropertyBase property, + BindingPriority maxPriority = BindingPriority.Animation) { var o = this; var inherits = property.Inherits; @@ -699,7 +696,7 @@ namespace Avalonia { var values = o._values; - if (values?.TryGetValue(property, out value) == true) + if (values?.TryGetValue(property, maxPriority, out value) == true) { return value; } @@ -715,6 +712,51 @@ namespace Avalonia return property.GetDefaultValue(GetType()); } + protected internal void RaisePropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + VerifyAccess(); + + if (change.IsEffectiveValueChange) + { + change.Property.Notifying?.Invoke(this, true); + } + + try + { + OnPropertyChangedCore(change); + + if (change.IsEffectiveValueChange) + { + change.Property.NotifyChanged(change); + _propertyChanged?.Invoke(this, change); + + if (_inpcChanged != null) + { + var inpce = new PropertyChangedEventArgs(change.Property.Name); + _inpcChanged(this, inpce); + } + + if (change.Property.Inherits && _inheritanceChildren != null) + { + foreach (var child in _inheritanceChildren) + { + child.InheritedPropertyChanged( + change.Property, + change.OldValue, + change.NewValue.ToOptional()); + } + } + } + } + finally + { + if (change.IsEffectiveValueChange) + { + change.Property.Notifying?.Invoke(this, false); + } + } + } + /// /// Sets the value of a direct property. /// diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 4fc65a3ed4..173c5c1a94 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -448,6 +448,65 @@ namespace Avalonia }; } + /// + /// Gets an base value. + /// + /// The object. + /// The property. + /// The maximum priority for the value. + /// + /// For styled properties, gets the value of the property if set on the object with a + /// priority equal or lower to , otherwise + /// . Note that this method does not return + /// property values that come from inherited or default values. + /// + /// For direct properties returns . + /// + public static object GetBaseValue( + this IAvaloniaObject target, + AvaloniaProperty property, + BindingPriority maxPriority) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + return property.RouteGetBaseValue(target, maxPriority); + } + + /// + /// Gets an base value. + /// + /// The object. + /// The property. + /// The maximum priority for the value. + /// + /// For styled properties, gets the value of the property if set on the object with a + /// priority equal or lower to , otherwise + /// . Note that this method does not return property values + /// that come from inherited or default values. + /// + /// For direct properties returns + /// . + /// + public static Optional GetBaseValue( + this IAvaloniaObject target, + AvaloniaProperty property, + BindingPriority maxPriority) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + return property switch + { + StyledPropertyBase styled => target.GetBaseValue(styled, maxPriority), + DirectPropertyBase direct => target.GetValue(direct), + _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") + }; + } + /// /// Sets a value. /// diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 74d7039751..daa7191cc5 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -496,6 +496,13 @@ namespace Avalonia /// The object instance. internal abstract object RouteGetValue(IAvaloniaObject o); + /// + /// Routes an untyped GetBaseValue call to a typed call. + /// + /// The object instance. + /// The maximum priority for the value. + internal abstract object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority); + /// /// Routes an untyped SetValue call to a typed call. /// diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs index 0f09747865..c1a2832fde 100644 --- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs @@ -16,6 +16,7 @@ namespace Avalonia { Sender = sender; Priority = priority; + IsEffectiveValueChange = true; } /// @@ -35,19 +36,11 @@ namespace Avalonia /// /// Gets the old value of the property. /// - /// - /// The old value of the property or if the - /// property previously had no value. - /// public object? OldValue => GetOldValue(); /// /// Gets the new value of the property. /// - /// - /// The new value of the property or if the - /// property previously had no value. - /// public object? NewValue => GetNewValue(); /// @@ -58,6 +51,20 @@ namespace Avalonia /// public BindingPriority Priority { get; private set; } + /// + /// Gets a value indicating whether the change represents a change to the effective value of + /// the property. + /// + /// + /// This will usually be true, except in + /// + /// which recieves notifications for all changes to property values, whether a value with a higher + /// priority is present or not. When this property is false, the change that is being signalled + /// has not resulted in a change to the property value on the object. + /// + public bool IsEffectiveValueChange { get; private set; } + + internal void MarkNonEffectiveValue() => IsEffectiveValueChange = false; protected abstract AvaloniaProperty GetProperty(); protected abstract object? GetOldValue(); protected abstract object? GetNewValue(); diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs index fca32b4ffc..054bf93b3a 100644 --- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs @@ -1,4 +1,3 @@ -using System; using Avalonia.Data; #nullable enable @@ -6,7 +5,7 @@ using Avalonia.Data; namespace Avalonia { /// - /// Provides information for a avalonia property change. + /// Provides information for an Avalonia property change. /// public class AvaloniaPropertyChangedEventArgs : AvaloniaPropertyChangedEventArgs { @@ -42,19 +41,28 @@ namespace Avalonia /// /// Gets the old value of the property. /// - /// - /// The old value of the property. - /// + /// + /// When is true, returns the + /// old value of the property on the object. + /// When is false, returns + /// . + /// public new Optional OldValue { get; private set; } /// /// Gets the new value of the property. /// - /// - /// The new value of the property. - /// + /// + /// When is true, returns the + /// value of the property on the object. + /// When is false returns the + /// changed value, or if the value was removed. + /// public new BindingValue NewValue { get; private set; } + internal void SetOldValue(Optional value) => OldValue = value; + internal void SetNewValue(BindingValue value) => NewValue = value; + protected override AvaloniaProperty GetProperty() => Property; protected override object? GetOldValue() => OldValue.GetValueOrDefault(AvaloniaProperty.UnsetValue); diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index cecdd33e7b..9aac1bacba 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -348,8 +348,8 @@ namespace Avalonia.Data return new BindingValue( fallbackValue.HasValue ? - BindingValueType.DataValidationError : - BindingValueType.DataValidationErrorWithFallback, + BindingValueType.DataValidationErrorWithFallback : + BindingValueType.DataValidationError, fallbackValue.HasValue ? fallbackValue.Value : default, e); } diff --git a/src/Avalonia.Base/Data/Converters/BoolConverters.cs b/src/Avalonia.Base/Data/Converters/BoolConverters.cs index 817d1cea9a..6740c2168f 100644 --- a/src/Avalonia.Base/Data/Converters/BoolConverters.cs +++ b/src/Avalonia.Base/Data/Converters/BoolConverters.cs @@ -3,7 +3,7 @@ using System.Linq; namespace Avalonia.Data.Converters { /// - /// Provides a set of useful s for working with string values. + /// Provides a set of useful s for working with bool values. /// public static class BoolConverters { diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index f2ed86d2aa..84ef0fb695 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -1,7 +1,5 @@ using System; using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; using System.Reflection; using Avalonia.Utilities; @@ -31,7 +29,11 @@ namespace Avalonia.Data.Core.Plugins Contract.Requires(propertyName != null); reference.TryGetTarget(out object instance); - var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(x => x.Name == propertyName); + + const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | + BindingFlags.Static | BindingFlags.Instance; + + var p = instance.GetType().GetProperty(propertyName, bindingFlags); if (p != null) { diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index c19ee8dba7..5d694f4cf9 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs @@ -1,13 +1,16 @@ using System; -using System.Linq; +using System.Collections.Generic; +using System.Linq.Expressions; using System.Reflection; namespace Avalonia.Data.Core.Plugins { - class MethodAccessorPlugin : IPropertyAccessorPlugin + public class MethodAccessorPlugin : IPropertyAccessorPlugin { - public bool Match(object obj, string methodName) - => obj.GetType().GetRuntimeMethods().Any(x => x.Name == methodName); + private readonly Dictionary<(Type, string), MethodInfo> _methodLookup = + new Dictionary<(Type, string), MethodInfo>(); + + public bool Match(object obj, string methodName) => GetFirstMethodWithName(obj.GetType(), methodName) != null; public IPropertyAccessor Start(WeakReference reference, string methodName) { @@ -15,17 +18,22 @@ namespace Avalonia.Data.Core.Plugins Contract.Requires(methodName != null); reference.TryGetTarget(out object instance); - var method = instance.GetType().GetRuntimeMethods().FirstOrDefault(x => x.Name == methodName); + + var method = GetFirstMethodWithName(instance.GetType(), methodName); if (method != null) { - if (method.GetParameters().Length + (method.ReturnType == typeof(void) ? 0 : 1) > 8) + var parameters = method.GetParameters(); + + if (parameters.Length + (method.ReturnType == typeof(void) ? 0 : 1) > 8) { - var exception = new ArgumentException("Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", nameof(methodName)); + var exception = new ArgumentException( + "Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", + nameof(methodName)); return new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); } - return new Accessor(reference, method); + return new Accessor(reference, method, parameters); } else { @@ -35,31 +43,72 @@ namespace Avalonia.Data.Core.Plugins } } + private MethodInfo GetFirstMethodWithName(Type type, string methodName) + { + var key = (type, methodName); + + if (!_methodLookup.TryGetValue(key, out MethodInfo methodInfo)) + { + methodInfo = TryFindAndCacheMethod(type, methodName); + } + + return methodInfo; + } + + private MethodInfo TryFindAndCacheMethod(Type type, string methodName) + { + MethodInfo found = null; + + const BindingFlags bindingFlags = + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance; + + var methods = type.GetMethods(bindingFlags); + + foreach (MethodInfo methodInfo in methods) + { + if (methodInfo.Name == methodName) + { + found = methodInfo; + + break; + } + } + + _methodLookup.Add((type, methodName), found); + + return found; + } + private sealed class Accessor : PropertyAccessorBase { - public Accessor(WeakReference reference, MethodInfo method) + public Accessor(WeakReference reference, MethodInfo method, ParameterInfo[] parameters) { Contract.Requires(reference != null); Contract.Requires(method != null); - var paramTypes = method.GetParameters().Select(param => param.ParameterType).ToArray(); var returnType = method.ReturnType; - - if (returnType == typeof(void)) + bool hasReturn = returnType != typeof(void); + + var signatureTypeCount = (hasReturn ? 1 : 0) + parameters.Length; + + var paramTypes = new Type[signatureTypeCount]; + + for (var i = 0; i < parameters.Length; i++) { - if (paramTypes.Length == 0) - { - PropertyType = typeof(Action); - } - else - { - PropertyType = Type.GetType($"System.Action`{paramTypes.Length}").MakeGenericType(paramTypes); - } + ParameterInfo parameter = parameters[i]; + + paramTypes[i] = parameter.ParameterType; + } + + if (hasReturn) + { + paramTypes[paramTypes.Length - 1] = returnType; + + PropertyType = Expression.GetFuncType(paramTypes); } else { - var genericTypeParameters = paramTypes.Concat(new[] { returnType }).ToArray(); - PropertyType = Type.GetType($"System.Func`{genericTypeParameters.Length}").MakeGenericType(genericTypeParameters); + PropertyType = Expression.GetActionType(paramTypes); } if (method.IsStatic) diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index d3b5277c53..0e65379abd 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -120,6 +120,11 @@ namespace Avalonia return o.GetValue(this); } + internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) + { + return o.GetValue(this); + } + /// internal override IDisposable? RouteSetValue( IAvaloniaObject o, diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index 867249bf0e..0452f77d4c 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -41,6 +41,19 @@ namespace Avalonia /// The value. T GetValue(DirectPropertyBase property); + /// + /// Gets an base value. + /// + /// The type of the property. + /// The property. + /// The maximum priority for the value. + /// + /// Gets the value of the property, if set on this object with a priority equal or lower to + /// , otherwise . Note that + /// this method does not return property values that come from inherited or default values. + /// + Optional GetBaseValue(StyledPropertyBase property, BindingPriority maxPriority); + /// /// Checks whether a is animating. /// diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs index 3249b31d66..0d563947e7 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs @@ -22,6 +22,7 @@ namespace Avalonia.PropertyStore private readonly IAvaloniaObject _owner; private IValueSink _sink; private IDisposable? _subscription; + private Optional _value; public BindingEntry( IAvaloniaObject owner, @@ -40,18 +41,21 @@ namespace Avalonia.PropertyStore public StyledPropertyBase Property { get; } public BindingPriority Priority { get; } public IObservable> Source { get; } - public Optional Value { get; private set; } - Optional IValue.Value => Value.ToObject(); - BindingPriority IValue.ValuePriority => Priority; + Optional IValue.GetValue() => _value.ToObject(); + + public Optional GetValue(BindingPriority maxPriority) + { + return Priority >= maxPriority ? _value : Optional.Empty; + } public void Dispose() { _subscription?.Dispose(); _subscription = null; - _sink.Completed(Property, this, Value); + _sink.Completed(Property, this, _value); } - public void OnCompleted() => _sink.Completed(Property, this, Value); + public void OnCompleted() => _sink.Completed(Property, this, _value); public void OnError(Exception error) { @@ -94,14 +98,14 @@ namespace Avalonia.PropertyStore return; } - var old = Value; + var old = _value; if (value.Type != BindingValueType.DataValidationError) { - Value = value.ToOptional(); + _value = value.ToOptional(); } - _sink.ValueChanged(Property, Priority, old, value); + _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(_owner, Property, old, value, Priority)); } } } diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs index aa054c46ff..46f6f9a137 100644 --- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs @@ -13,6 +13,7 @@ namespace Avalonia.PropertyStore internal class ConstantValueEntry : IPriorityValueEntry, IDisposable { private IValueSink _sink; + private Optional _value; public ConstantValueEntry( StyledPropertyBase property, @@ -21,18 +22,21 @@ namespace Avalonia.PropertyStore IValueSink sink) { Property = property; - Value = value; + _value = value; Priority = priority; _sink = sink; } public StyledPropertyBase Property { get; } public BindingPriority Priority { get; } - public Optional Value { get; } - Optional IValue.Value => Value.ToObject(); - BindingPriority IValue.ValuePriority => Priority; + Optional IValue.GetValue() => _value.ToObject(); - public void Dispose() => _sink.Completed(Property, this, Value); + public Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation) + { + return Priority >= maxPriority ? _value : Optional.Empty; + } + + public void Dispose() => _sink.Completed(Property, this, _value); public void Reparent(IValueSink sink) => _sink = sink; } } diff --git a/src/Avalonia.Base/PropertyStore/IValue.cs b/src/Avalonia.Base/PropertyStore/IValue.cs index 0ce7fb8308..249cfc360c 100644 --- a/src/Avalonia.Base/PropertyStore/IValue.cs +++ b/src/Avalonia.Base/PropertyStore/IValue.cs @@ -9,8 +9,8 @@ namespace Avalonia.PropertyStore /// internal interface IValue { - Optional Value { get; } - BindingPriority ValuePriority { get; } + Optional GetValue(); + BindingPriority Priority { get; } } /// @@ -19,6 +19,6 @@ namespace Avalonia.PropertyStore /// The property type. internal interface IValue : IValue { - new Optional Value { get; } + Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation); } } diff --git a/src/Avalonia.Base/PropertyStore/IValueSink.cs b/src/Avalonia.Base/PropertyStore/IValueSink.cs index 9012a985ac..3a1e9731d8 100644 --- a/src/Avalonia.Base/PropertyStore/IValueSink.cs +++ b/src/Avalonia.Base/PropertyStore/IValueSink.cs @@ -9,11 +9,7 @@ namespace Avalonia.PropertyStore /// internal interface IValueSink { - void ValueChanged( - StyledPropertyBase property, - BindingPriority priority, - Optional oldValue, - BindingValue newValue); + void ValueChanged(AvaloniaPropertyChangedEventArgs change); void Completed( StyledPropertyBase property, diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs index 22258390da..59c017bc09 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs @@ -14,9 +14,14 @@ namespace Avalonia.PropertyStore private T _value; public LocalValueEntry(T value) => _value = value; - public Optional Value => _value; - public BindingPriority ValuePriority => BindingPriority.LocalValue; - Optional IValue.Value => Value.ToObject(); + public BindingPriority Priority => BindingPriority.LocalValue; + Optional IValue.GetValue() => new Optional(_value); + + public Optional GetValue(BindingPriority maxPriority) + { + return BindingPriority.LocalValue >= maxPriority ? _value : Optional.Empty; + } + public void SetValue(T value) => _value = value; } } diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index affb20f334..5e223cad60 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Avalonia.Data; #nullable enable @@ -24,6 +25,7 @@ namespace Avalonia.PropertyStore private readonly List> _entries = new List>(); private readonly Func? _coerceValue; private Optional _localValue; + private Optional _value; public PriorityValue( IAvaloniaObject owner, @@ -50,11 +52,13 @@ namespace Avalonia.PropertyStore { existing.Reparent(this); _entries.Add(existing); + + var v = existing.GetValue(); - if (existing.Value.HasValue) + if (v.HasValue) { - Value = existing.Value; - ValuePriority = existing.Priority; + _value = v; + Priority = existing.Priority; } } @@ -65,18 +69,39 @@ namespace Avalonia.PropertyStore LocalValueEntry existing) : this(owner, property, sink) { - _localValue = existing.Value; - Value = _localValue; - ValuePriority = BindingPriority.LocalValue; + _value = _localValue = existing.GetValue(BindingPriority.LocalValue); + Priority = BindingPriority.LocalValue; } public StyledPropertyBase Property { get; } - public Optional Value { get; private set; } - public BindingPriority ValuePriority { get; private set; } + public BindingPriority Priority { get; private set; } = BindingPriority.Unset; public IReadOnlyList> Entries => _entries; - Optional IValue.Value => Value.ToObject(); + Optional IValue.GetValue() => _value.ToObject(); + + public void ClearLocalValue() + { + UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( + _owner, + Property, + default, + default, + BindingPriority.LocalValue)); + } + + public Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation) + { + if (Priority == BindingPriority.Unset) + { + return default; + } - public void ClearLocalValue() => UpdateEffectiveValue(); + if (Priority >= maxPriority) + { + return _value; + } + + return CalculateValue(maxPriority).Item1; + } public IDisposable? SetValue(T value, BindingPriority priority) { @@ -94,7 +119,13 @@ namespace Avalonia.PropertyStore result = entry; } - UpdateEffectiveValue(); + UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( + _owner, + Property, + default, + value, + priority)); + return result; } @@ -106,20 +137,19 @@ namespace Avalonia.PropertyStore return binding; } - public void CoerceValue() => UpdateEffectiveValue(); + public void CoerceValue() => UpdateEffectiveValue(null); - void IValueSink.ValueChanged( - StyledPropertyBase property, - BindingPriority priority, - Optional oldValue, - BindingValue newValue) + void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change) { - if (priority == BindingPriority.LocalValue) + if (change.Priority == BindingPriority.LocalValue) { _localValue = default; } - UpdateEffectiveValue(); + if (change is AvaloniaPropertyChangedEventArgs c) + { + UpdateEffectiveValue(c); + } } void IValueSink.Completed( @@ -128,7 +158,16 @@ namespace Avalonia.PropertyStore Optional oldValue) { _entries.Remove((IPriorityValueEntry)entry); - UpdateEffectiveValue(); + + if (oldValue is Optional o) + { + UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( + _owner, + Property, + o, + default, + entry.Priority)); + } } private int FindInsertPoint(BindingPriority priority) @@ -147,53 +186,73 @@ namespace Avalonia.PropertyStore return result; } - private void UpdateEffectiveValue() + public (Optional, BindingPriority) CalculateValue(BindingPriority maxPriority) { var reachedLocalValues = false; - var value = default(Optional); - if (_entries.Count > 0) + for (var i = _entries.Count - 1; i >= 0; --i) { - for (var i = _entries.Count - 1; i >= 0; --i) + var entry = _entries[i]; + + if (entry.Priority < maxPriority) + { + continue; + } + + if (!reachedLocalValues && + entry.Priority >= BindingPriority.LocalValue && + maxPriority <= BindingPriority.LocalValue && + _localValue.HasValue) + { + return (_localValue, BindingPriority.LocalValue); + } + + var entryValue = entry.GetValue(); + + if (entryValue.HasValue) { - var entry = _entries[i]; - - if (!reachedLocalValues && entry.Priority >= BindingPriority.LocalValue) - { - reachedLocalValues = true; - - if (_localValue.HasValue) - { - value = _localValue; - ValuePriority = BindingPriority.LocalValue; - break; - } - } - - if (entry.Value.HasValue) - { - value = entry.Value; - ValuePriority = entry.Priority; - break; - } + return (entryValue, entry.Priority); } } - else if (_localValue.HasValue) + + if (!reachedLocalValues && + maxPriority <= BindingPriority.LocalValue && + _localValue.HasValue) { - value = _localValue; - ValuePriority = BindingPriority.LocalValue; + return (_localValue, BindingPriority.LocalValue); } + return (default, BindingPriority.Unset); + } + + private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs? change) + { + var (value, priority) = CalculateValue(BindingPriority.Animation); + if (value.HasValue && _coerceValue != null) { value = _coerceValue(_owner, value.Value); } - if (value != Value) + Priority = priority; + + if (value != _value) + { + var old = _value; + _value = value; + + _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + _owner, + Property, + old, + value, + Priority)); + } + else if (change is object) { - var old = Value; - Value = value; - _sink.ValueChanged(Property, ValuePriority, old, value); + change.MarkNonEffectiveValue(); + change.SetOldValue(default); + _sink.ValueChanged(change); } } } diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index 1f88bfb2aa..3e92c3bdf7 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -197,6 +197,13 @@ namespace Avalonia return o.GetValue(this); } + /// + internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) + { + var value = o.GetBaseValue(this, maxPriority); + return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue; + } + /// internal override IDisposable RouteSetValue( IAvaloniaObject o, diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 66b4676b45..d0d88166a7 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -115,45 +115,46 @@ namespace Avalonia.Utilities return true; } + var toUnderl = Nullable.GetUnderlyingType(to) ?? to; var from = value.GetType(); - if (to.IsAssignableFrom(from)) + if (toUnderl.IsAssignableFrom(from)) { result = value; return true; } - if (to == typeof(string)) + if (toUnderl == typeof(string)) { - result = Convert.ToString(value); + result = Convert.ToString(value, culture); return true; } - if (to.IsEnum && from == typeof(string)) + if (toUnderl.IsEnum && from == typeof(string)) { - if (Enum.IsDefined(to, (string)value)) + if (Enum.IsDefined(toUnderl, (string)value)) { - result = Enum.Parse(to, (string)value); + result = Enum.Parse(toUnderl, (string)value); return true; } } - if (!from.IsEnum && to.IsEnum) + if (!from.IsEnum && toUnderl.IsEnum) { result = null; - if (TryConvert(Enum.GetUnderlyingType(to), value, culture, out object enumValue)) + if (TryConvert(Enum.GetUnderlyingType(toUnderl), value, culture, out object enumValue)) { - result = Enum.ToObject(to, enumValue); + result = Enum.ToObject(toUnderl, enumValue); return true; } } - if (from.IsEnum && IsNumeric(to)) + if (from.IsEnum && IsNumeric(toUnderl)) { try { - result = Convert.ChangeType((int)value, to, culture); + result = Convert.ChangeType((int)value, toUnderl, culture); return true; } catch @@ -164,7 +165,7 @@ namespace Avalonia.Utilities } var convertableFrom = Array.IndexOf(InbuiltTypes, from); - var convertableTo = Array.IndexOf(InbuiltTypes, to); + var convertableTo = Array.IndexOf(InbuiltTypes, toUnderl); if (convertableFrom != -1 && convertableTo != -1) { @@ -172,7 +173,7 @@ namespace Avalonia.Utilities { try { - result = Convert.ChangeType(value, to, culture); + result = Convert.ChangeType(value, toUnderl, culture); return true; } catch @@ -183,15 +184,23 @@ namespace Avalonia.Utilities } } - var typeConverter = TypeDescriptor.GetConverter(to); + var toTypeConverter = TypeDescriptor.GetConverter(toUnderl); + + if (toTypeConverter.CanConvertFrom(from) == true) + { + result = toTypeConverter.ConvertFrom(null, culture, value); + return true; + } + + var fromTypeConverter = TypeDescriptor.GetConverter(from); - if (typeConverter.CanConvertFrom(from) == true) + if (fromTypeConverter.CanConvertTo(toUnderl) == true) { - result = typeConverter.ConvertFrom(null, culture, value); + result = fromTypeConverter.ConvertTo(null, culture, value, toUnderl); return true; } - var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit | OperatorType.Explicit); + var cast = FindTypeConversionOperatorMethod(from, toUnderl, OperatorType.Implicit | OperatorType.Explicit); if (cast != null) { diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 104c06de0f..05e66f2e0a 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -37,7 +37,7 @@ namespace Avalonia { if (_values.TryGetValue(property, out var slot)) { - return slot.ValuePriority < BindingPriority.LocalValue; + return slot.Priority < BindingPriority.LocalValue; } return false; @@ -47,21 +47,24 @@ namespace Avalonia { if (_values.TryGetValue(property, out var slot)) { - return slot.Value.HasValue; + return slot.GetValue().HasValue; } return false; } - public bool TryGetValue(StyledPropertyBase property, out T value) + public bool TryGetValue( + StyledPropertyBase property, + BindingPriority maxPriority, + out T value) { if (_values.TryGetValue(property, out var slot)) { - var v = (IValue)slot; + var v = ((IValue)slot).GetValue(maxPriority); - if (v.Value.HasValue) + if (v.HasValue) { - value = v.Value.Value; + value = v.Value; return true; } } @@ -90,17 +93,22 @@ namespace Avalonia _values.AddValue(property, entry); result = entry.SetValue(value, priority); } - else if (priority == BindingPriority.LocalValue) - { - _values.AddValue(property, new LocalValueEntry(value)); - _sink.ValueChanged(property, priority, default, value); - } else { - var entry = new ConstantValueEntry(property, value, priority, this); - _values.AddValue(property, entry); - _sink.ValueChanged(property, priority, default, value); - result = entry; + var change = new AvaloniaPropertyChangedEventArgs(_owner, property, default, value, priority); + + if (priority == BindingPriority.LocalValue) + { + _values.AddValue(property, new LocalValueEntry(value)); + _sink.ValueChanged(change); + } + else + { + var entry = new ConstantValueEntry(property, value, priority, this); + _values.AddValue(property, entry); + _sink.ValueChanged(change); + result = entry; + } } return result; @@ -149,13 +157,14 @@ namespace Avalonia if (remove) { - var old = TryGetValue(property, out var value) ? value : default; + var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default; _values.Remove(property); - _sink.ValueChanged( + _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + _owner, property, - BindingPriority.Unset, old, - BindingValue.Unset); + default, + BindingPriority.Unset)); } } } @@ -176,23 +185,20 @@ namespace Avalonia { if (_values.TryGetValue(property, out var slot)) { + var slotValue = slot.GetValue(); return new Diagnostics.AvaloniaPropertyValue( property, - slot.Value.HasValue ? slot.Value.Value : AvaloniaProperty.UnsetValue, - slot.ValuePriority, + slotValue.HasValue ? slotValue.Value : AvaloniaProperty.UnsetValue, + slot.Priority, null); } return null; } - void IValueSink.ValueChanged( - StyledPropertyBase property, - BindingPriority priority, - Optional oldValue, - BindingValue newValue) + void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change) { - _sink.ValueChanged(property, priority, oldValue, newValue); + _sink.ValueChanged(change); } void IValueSink.Completed( @@ -232,9 +238,14 @@ namespace Avalonia { if (priority == BindingPriority.LocalValue) { - var old = l.Value; + var old = l.GetValue(BindingPriority.LocalValue); l.SetValue(value); - _sink.ValueChanged(property, priority, old, value); + _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + _owner, + property, + old, + value, + priority)); } else { diff --git a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs index 39ee3f6bca..95e59dde2b 100644 --- a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs +++ b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs @@ -12,6 +12,8 @@ namespace Avalonia.Build.Tasks { public bool Execute() { + Enum.TryParse(ReportImportance, true, out MessageImportance outputImportance); + OutputPath = OutputPath ?? AssemblyFile; var outputPdb = GetPdbPath(OutputPath); var input = AssemblyFile; @@ -32,9 +34,12 @@ namespace Avalonia.Build.Tasks } } + var msg = $"CompileAvaloniaXamlTask -> AssemblyFile:{AssemblyFile}, ProjectDirectory:{ProjectDirectory}, OutputPath:{OutputPath}"; + BuildEngine.LogMessage(msg, outputImportance < MessageImportance.Low ? MessageImportance.High : outputImportance); + var res = XamlCompilerTaskExecutor.Compile(BuildEngine, input, File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(), - ProjectDirectory, OutputPath, VerifyIl); + ProjectDirectory, OutputPath, VerifyIl, outputImportance); if (!res.Success) return false; if (!res.WrittenFile) @@ -68,7 +73,9 @@ namespace Avalonia.Build.Tasks public string OutputPath { get; set; } public bool VerifyIl { get; set; } - + + public string ReportImportance { get; set; } + public IBuildEngine BuildEngine { get; set; } public ITaskHost HostObject { get; set; } } diff --git a/src/Avalonia.Build.Tasks/Extensions.cs b/src/Avalonia.Build.Tasks/Extensions.cs index 440c6d7489..46c12eaf3d 100644 --- a/src/Avalonia.Build.Tasks/Extensions.cs +++ b/src/Avalonia.Build.Tasks/Extensions.cs @@ -9,14 +9,19 @@ namespace Avalonia.Build.Tasks public static void LogError(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message) { - engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message, + engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message, "", "Avalonia")); } - + public static void LogWarning(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message) { engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message, "", "Avalonia")); } + + public static void LogMessage(this IBuildEngine engine, string message, MessageImportance imp) + { + engine.LogMessageEvent(new BuildMessageEventArgs(message, "", "Avalonia", imp)); + } } } diff --git a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs index 98ebb3e7d1..406abe6f99 100644 --- a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs +++ b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs @@ -22,6 +22,10 @@ namespace Avalonia.Build.Tasks [Required] public ITaskItem[] EmbeddedResources { get; set; } + public string ReportImportance { get; set; } + + private MessageImportance _reportImportance; + class Source { public string Path { get; set; } @@ -29,15 +33,11 @@ namespace Avalonia.Build.Tasks private byte[] _data; private string _sourcePath; - public Source(string file, string root) + public Source(string relativePath, string root) { - file = SPath.GetFullPath(file); root = SPath.GetFullPath(root); - var fileUri = new Uri(file, UriKind.Absolute); - var rootUri = new Uri(root, UriKind.Absolute); - rootUri = new Uri(rootUri.ToString().TrimEnd('/') + '/'); - Path = '/' + rootUri.MakeRelativeUri(fileUri).ToString().TrimStart('/'); - _sourcePath = file; + Path = "/" + relativePath.Replace('\\', '/'); + _sourcePath = SPath.Combine(root, relativePath); Size = (int)new FileInfo(_sourcePath).Length; } @@ -65,7 +65,14 @@ namespace Avalonia.Build.Tasks } } - List BuildResourceSources() => Resources.Select(r => new Source(r.ItemSpec, Root)).ToList(); + List BuildResourceSources() + => Resources.Select(r => + { + + var src = new Source(r.ItemSpec, Root); + BuildEngine.LogMessage($"avares -> name:{src.Path}, path: {src.SystemPath}, size:{src.Size}, ItemSpec:{r.ItemSpec}", _reportImportance); + return src; + }).ToList(); private void Pack(Stream output, List sources) { @@ -136,10 +143,14 @@ namespace Avalonia.Build.Tasks sources.Add(new Source("/!AvaloniaResourceXamlInfo", ms.ToArray())); return true; } - + public bool Execute() { - foreach(var r in EmbeddedResources.Where(r=>r.ItemSpec.EndsWith(".xaml")||r.ItemSpec.EndsWith(".paml"))) + Enum.TryParse(ReportImportance, out _reportImportance); + + BuildEngine.LogMessage($"GenerateAvaloniaResourcesTask -> Root: {Root}, {Resources?.Count()} resources, Output:{Output}", _reportImportance < MessageImportance.Low ? MessageImportance.High : _reportImportance); + + foreach (var r in EmbeddedResources.Where(r => r.ItemSpec.EndsWith(".xaml") || r.ItemSpec.EndsWith(".paml"))) BuildEngine.LogWarning(BuildEngineErrorCode.LegacyResmScheme, r.ItemSpec, "XAML file is packed using legacy EmbeddedResource/resm scheme, relative URIs won't work"); var resources = BuildResourceSources(); diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index e348eb0fbc..3b69109e68 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -40,7 +40,7 @@ namespace Avalonia.Build.Tasks } public static CompileResult Compile(IBuildEngine engine, string input, string[] references, string projectDirectory, - string output, bool verifyIl) + string output, bool verifyIl, MessageImportance logImportance) { var typeSystem = new CecilTypeSystem(references.Concat(new[] {input}), input); var asm = typeSystem.TargetAssemblyDefinition; @@ -121,6 +121,8 @@ namespace Avalonia.Build.Tasks { try { + engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", logImportance); + // StreamReader is needed here to handle BOM var xaml = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd(); var parsed = XDocumentXamlIlParser.Parse(xaml); diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 3a1e612a05..cfe47a09d5 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -1,5 +1,4 @@ -// (c) Copyright Microsoft Corporation. -// This source is subject to the Microsoft Public License (Ms-PL). +// This source is subject to the Microsoft Public License (Ms-PL). // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. @@ -767,7 +766,7 @@ namespace Avalonia.Controls /// /// ItemsProperty property changed handler. /// - /// AvaloniaPropertyChangedEventArgs. + /// The event arguments. private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e) { if (!_areHandlersSuspended) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 5cae798dc8..3bf72460df 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -30,7 +30,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceNode + public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost { /// /// The application-global data templates. @@ -129,26 +129,13 @@ namespace Avalonia /// public IResourceDictionary Resources { - get => _resources ?? (Resources = new ResourceDictionary()); + get => _resources ??= new ResourceDictionary(this); set { - Contract.Requires(value != null); - - var hadResources = false; - - if (_resources != null) - { - hadResources = _resources.Count > 0; - _resources.ResourcesChanged -= ThisResourcesChanged; - } - + value = value ?? throw new ArgumentNullException(nameof(value)); + _resources?.RemoveOwner(this); _resources = value; - _resources.ResourcesChanged += ThisResourcesChanged; - - if (hadResources || _resources.Count > 0) - { - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - } + _resources.AddOwner(this); } } @@ -161,23 +148,15 @@ namespace Avalonia /// /// Global styles apply to all windows in the application. /// - public Styles Styles - { - get - { - if (_styles == null) - { - _styles = new Styles(this); - _styles.ResourcesChanged += ThisResourcesChanged; - } - - return _styles; - } - } + public Styles Styles => _styles ??= new Styles(this); /// bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; + /// + bool IResourceNode.HasResources => (_resources?.HasResources ?? false) || + (((IResourceNode?)_styles)?.HasResources ?? false); + /// /// Gets the styling parent of the application, which is null. /// @@ -185,13 +164,7 @@ namespace Avalonia /// bool IStyleHost.IsStylesInitialized => _styles != null; - - /// - bool IResourceProvider.HasResources => _resources?.Count > 0; - - /// - IResourceNode IResourceNode.ResourceParent => null; - + /// /// Application lifetime, use it for things like setting the main window and exiting the app from code /// Currently supported lifetimes are: @@ -219,13 +192,18 @@ namespace Avalonia public virtual void Initialize() { } /// - bool IResourceProvider.TryGetResource(object key, out object value) + bool IResourceNode.TryGetResource(object key, out object value) { value = null; return (_resources?.TryGetResource(key, out value) ?? false) || Styles.TryGetResource(key, out value); } + void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e) + { + ResourcesChanged?.Invoke(this, e); + } + void IStyleHost.StylesAdded(IReadOnlyList styles) { _stylesAdded?.Invoke(styles); @@ -254,8 +232,12 @@ namespace Avalonia .Bind().ToTransient() .Bind().ToConstant(_styler) .Bind().ToConstant(AvaloniaScheduler.Instance) - .Bind().ToConstant(DragDropDevice.Instance) - .Bind().ToTransient(); + .Bind().ToConstant(DragDropDevice.Instance); + + // TODO: Fix this, for now we keep this behavior since someone might be relying on it in 0.9.x + if (AvaloniaLocator.Current.GetService() == null) + AvaloniaLocator.CurrentMutable + .Bind().ToTransient(); var clock = new RenderLoopClock(); AvaloniaLocator.CurrentMutable @@ -278,9 +260,7 @@ namespace Avalonia try { _notifyingResourcesChanged = true; - (_resources as ISetResourceParent)?.ParentResourcesChanged(e); - (_styles as ISetResourceParent)?.ParentResourcesChanged(e); - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + ResourcesChanged?.Invoke(this, ResourcesChangedEventArgs.Empty); } finally { diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index c62ad3030e..31101dc0f1 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -10,6 +10,7 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; +using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Avalonia.Collections; @@ -1101,6 +1102,7 @@ namespace Avalonia.Controls { _textBoxSubscriptions = _textBox.GetObservable(TextBox.TextProperty) + .Skip(1) .Subscribe(_ => OnTextBoxTextChanged()); if (Text != null) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 136e8ed851..b54eb2ac57 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -313,17 +313,13 @@ namespace Avalonia.Controls IsPressed = false; } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == IsPressedProperty) + if (change.Property == IsPressedProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault()); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index 7945d63b06..44f66d397a 100644 --- a/src/Avalonia.Controls/ButtonSpinner.cs +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -205,17 +205,13 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == ButtonSpinnerLocationProperty) + if (change.Property == ButtonSpinnerLocationProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault()); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs index 7cd0230b36..0f53dc1364 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -510,17 +510,13 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == SelectedDateProperty) + if (change.Property == SelectedDateProperty) { - DataValidationErrors.SetError(this, newValue.Error); + DataValidationErrors.SetError(this, change.NewValue.Error); } } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index e71bcc24a7..6557346b1f 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -51,6 +51,18 @@ namespace Avalonia.Controls public static readonly StyledProperty VirtualizationModeProperty = ItemsPresenter.VirtualizationModeProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty PlaceholderTextProperty = + AvaloniaProperty.Register(nameof(PlaceholderText)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PlaceholderForegroundProperty = + AvaloniaProperty.Register(nameof(PlaceholderForeground)); + private bool _isDropDownOpen; private Popup _popup; private object _selectionBoxItem; @@ -94,6 +106,24 @@ namespace Avalonia.Controls set { SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); } } + /// + /// Gets or sets the PlaceHolder text. + /// + public string PlaceholderText + { + get { return GetValue(PlaceholderTextProperty); } + set { SetValue(PlaceholderTextProperty, value); } + } + + /// + /// Gets or sets the Brush that renders the placeholder text. + /// + public IBrush PlaceholderForeground + { + get { return GetValue(PlaceholderForegroundProperty); } + set { SetValue(PlaceholderForegroundProperty, value); } + } + /// /// Gets or sets the virtualization mode for the items. /// diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 1735599988..86499530da 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Linq; using Avalonia.Controls.Generators; @@ -9,18 +10,19 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.LogicalTree; +using Avalonia.Styling; namespace Avalonia.Controls { /// /// A control context menu. /// - public class ContextMenu : MenuBase + public class ContextMenu : MenuBase, ISetterValue { private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel { Orientation = Orientation.Vertical }); private Popup _popup; - private Control _attachedControl; + private List _attachedControls; private IInputElement _previousFocus; /// @@ -74,13 +76,14 @@ namespace Avalonia.Controls if (e.OldValue is ContextMenu oldMenu) { control.PointerReleased -= ControlPointerReleased; - oldMenu._attachedControl = null; + oldMenu._attachedControls?.Remove(control); ((ISetLogicalParent)oldMenu._popup)?.SetParent(null); } if (e.NewValue is ContextMenu newMenu) { - newMenu._attachedControl = control; + newMenu._attachedControls ??= new List(); + newMenu._attachedControls.Add(control); control.PointerReleased += ControlPointerReleased; } } @@ -96,18 +99,22 @@ namespace Avalonia.Controls /// The control. public void Open(Control control) { - if (control is null && _attachedControl is null) + if (control is null && (_attachedControls is null || _attachedControls.Count == 0)) { throw new ArgumentNullException(nameof(control)); } - if (control is object && _attachedControl is object && control != _attachedControl) + if (control is object && + _attachedControls is object && + !_attachedControls.Contains(control)) { throw new ArgumentException( "Cannot show ContentMenu on a different control to the one it is attached to.", nameof(control)); } + control ??= _attachedControls[0]; + if (IsOpen) { return; @@ -126,7 +133,12 @@ namespace Avalonia.Controls _popup.Closed += PopupClosed; } - ((ISetLogicalParent)_popup).SetParent(control); + if (_popup.Parent != control) + { + ((ISetLogicalParent)_popup).SetParent(null); + ((ISetLogicalParent)_popup).SetParent(control); + } + _popup.Child = this; _popup.IsOpen = true; @@ -155,6 +167,17 @@ namespace Avalonia.Controls } } + void ISetterValue.Initialize(ISetter setter) + { + // ContextMenu can be assigned to the ContextMenu property in a setter. This overrides + // the behavior defined in Control which requires controls to be wrapped in a