Browse Source

Merge branch 'master' into fix-controlcatatol-person

pull/4053/head
danwalmsley 6 years ago
committed by GitHub
parent
commit
d7aa3d670c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      Avalonia.sln
  2. 2
      Documentation/build.md
  3. 2
      build.sh
  4. 1
      build/CoreLibraries.props
  5. 58
      native/Avalonia.Native/inc/avalonia-native.h
  6. 4
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  7. 4
      native/Avalonia.Native/src/OSX/AvnString.h
  8. 61
      native/Avalonia.Native/src/OSX/AvnString.mm
  9. 124
      native/Avalonia.Native/src/OSX/clipboard.mm
  10. 8
      native/Avalonia.Native/src/OSX/common.h
  11. 89
      native/Avalonia.Native/src/OSX/dnd.mm
  12. 19
      native/Avalonia.Native/src/OSX/main.mm
  13. 8
      native/Avalonia.Native/src/OSX/window.h
  14. 211
      native/Avalonia.Native/src/OSX/window.mm
  15. 5
      packages/Avalonia/AvaloniaBuildTasks.targets
  16. 33
      readme.md
  17. 4
      samples/ControlCatalog/App.xaml
  18. 53
      samples/ControlCatalog/App.xaml.cs
  19. 17
      samples/ControlCatalog/MainView.xaml
  20. 30
      samples/ControlCatalog/MainView.xaml.cs
  21. 3
      samples/ControlCatalog/MainWindow.xaml
  22. 6
      samples/ControlCatalog/Pages/ButtonPage.xaml
  23. 20
      samples/ControlCatalog/Pages/ButtonPage.xaml.cs
  24. 18
      samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml
  25. 14
      samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml.cs
  26. 2
      samples/ControlCatalog/Pages/ComboBoxPage.xaml
  27. 2
      samples/ControlCatalog/Pages/DialogsPage.xaml
  28. 15
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  29. 12
      samples/ControlCatalog/Pages/DragAndDropPage.xaml
  30. 92
      samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
  31. 12
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  32. 2
      samples/ControlCatalog/Pages/SliderPage.xaml
  33. 2
      samples/ControlCatalog/Pages/ToolTipPage.xaml
  34. 2
      samples/ControlCatalog/Pages/TreeViewPage.xaml
  35. 100
      samples/ControlCatalog/Pages/TreeViewPage.xaml.cs
  36. 126
      samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs
  37. 1
      samples/Directory.Build.props
  38. 53
      samples/RenderDemo/Controls/LineBoundsDemoControl.cs
  39. 6
      samples/RenderDemo/MainWindow.xaml
  40. 9
      samples/RenderDemo/Pages/LineBoundsPage.xaml
  41. 19
      samples/RenderDemo/Pages/LineBoundsPage.xaml.cs
  42. 101
      samples/RenderDemo/Pages/TransitionsPage.xaml
  43. 37
      samples/RenderDemo/Pages/TransitionsPage.xaml.cs
  44. 3
      samples/RenderDemo/RenderDemo.csproj
  45. 6
      src/Android/Avalonia.Android/Platform/ClipboardImpl.cs
  46. 227
      src/Avalonia.Animation/Animatable.cs
  47. 2
      src/Avalonia.Animation/TransitionInstance.cs
  48. 13
      src/Avalonia.Animation/Transitions.cs
  49. 204
      src/Avalonia.Base/AvaloniaObject.cs
  50. 59
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  51. 7
      src/Avalonia.Base/AvaloniaProperty.cs
  52. 23
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
  53. 24
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs
  54. 4
      src/Avalonia.Base/Data/BindingValue.cs
  55. 2
      src/Avalonia.Base/Data/Converters/BoolConverters.cs
  56. 15
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  57. 93
      src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
  58. 9
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  59. 5
      src/Avalonia.Base/DirectPropertyBase.cs
  60. 13
      src/Avalonia.Base/IAvaloniaObject.cs
  61. 3
      src/Avalonia.Base/Properties/AssemblyInfo.cs
  62. 20
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  63. 14
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  64. 6
      src/Avalonia.Base/PropertyStore/IValue.cs
  65. 6
      src/Avalonia.Base/PropertyStore/IValueSink.cs
  66. 11
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  67. 161
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  68. 7
      src/Avalonia.Base/StyledPropertyBase.cs
  69. 139
      src/Avalonia.Base/Utilities/MathUtilities.cs
  70. 43
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  71. 69
      src/Avalonia.Base/ValueStore.cs
  72. 11
      src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs
  73. 9
      src/Avalonia.Build.Tasks/Extensions.cs
  74. 31
      src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs
  75. 4
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  76. 43
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  77. 2
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  78. 16
      src/Avalonia.Controls.DataGrid/DataGridColumns.cs
  79. 2
      src/Avalonia.Controls.DataGrid/DataGridLength.cs
  80. 2
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  81. 60
      src/Avalonia.Controls.DataGrid/DataGridRows.cs
  82. 6
      src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
  83. 136
      src/Avalonia.Controls.DataGrid/Utils/DoubleUtil.cs
  84. 68
      src/Avalonia.Controls/Application.cs
  85. 23
      src/Avalonia.Controls/AutoCompleteBox.cs
  86. 12
      src/Avalonia.Controls/Button.cs
  87. 12
      src/Avalonia.Controls/ButtonSpinner.cs
  88. 4
      src/Avalonia.Controls/Calendar/Calendar.cs
  89. 146
      src/Avalonia.Controls/Calendar/CalendarDatePicker.cs
  90. 4
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  91. 30
      src/Avalonia.Controls/ComboBox.cs
  92. 202
      src/Avalonia.Controls/ContextMenu.cs
  93. 22
      src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs
  94. 64
      src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs
  95. 8
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  96. 12
      src/Avalonia.Controls/Expander.cs
  97. 35
      src/Avalonia.Controls/Grid.cs
  98. 20
      src/Avalonia.Controls/IndexPath.cs
  99. 47
      src/Avalonia.Controls/IndexRange.cs
  100. 6
      src/Avalonia.Controls/LayoutTransformControl.cs

26
Avalonia.sln

@ -206,6 +206,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src
EndProject 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}" 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 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 Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 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|iPhone.Build.0 = Release|Any CPU
{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.ActiveCfg = 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 {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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

2
Documentation/build.md

@ -36,7 +36,7 @@ Avalonia requires [CastXML](https://github.com/CastXML/CastXML) for XML processi
On macOS: 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): On Debian based Linux (Debian, Ubuntu, Mint, etc):

2
build.sh

@ -67,6 +67,8 @@ else
fi fi
fi fi
export PATH=$DOTNET_DIRECTORY:$PATH
echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)"
"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]} "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]}

1
build/CoreLibraries.props

@ -11,6 +11,7 @@
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Visuals/Avalonia.Visuals.csproj" /> <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Visuals/Avalonia.Visuals.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Styling/Avalonia.Styling.csproj" /> <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Styling/Avalonia.Styling.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj" /> <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.OpenGL/Avalonia.OpenGL.csproj" /> <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.OpenGL/Avalonia.OpenGL.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Dialogs/Avalonia.Dialogs.csproj" /> <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Dialogs/Avalonia.Dialogs.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Markup/Avalonia.Markup/Avalonia.Markup.csproj" /> <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Markup/Avalonia.Markup/Avalonia.Markup.csproj" />

58
native/Avalonia.Native/inc/avalonia-native.h

@ -22,6 +22,9 @@ struct IAvnGlSurfaceRenderTarget;
struct IAvnGlSurfaceRenderingSession; struct IAvnGlSurfaceRenderingSession;
struct IAvnMenu; struct IAvnMenu;
struct IAvnMenuItem; struct IAvnMenuItem;
struct IAvnStringArray;
struct IAvnDndResultCallback;
struct IAvnGCHandleDeallocatorCallback;
struct IAvnMenuEvents; struct IAvnMenuEvents;
enum SystemDecorations { enum SystemDecorations {
@ -130,6 +133,22 @@ enum AvnInputModifiers
XButton2MouseButton = 256 XButton2MouseButton = 256
}; };
enum class AvnDragDropEffects
{
None = 0,
Copy = 1,
Move = 2,
Link = 4,
};
enum class AvnDragEventType
{
Enter,
Over,
Leave,
Drop
};
enum AvnWindowState enum AvnWindowState
{ {
Normal, Normal,
@ -188,7 +207,7 @@ enum AvnMenuItemToggleType
AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown
{ {
public: public:
virtual HRESULT Initialize() = 0; virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) = 0;
virtual IAvnMacOptions* GetMacOptions() = 0; virtual IAvnMacOptions* GetMacOptions() = 0;
virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv) = 0; virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv) = 0;
virtual HRESULT CreatePopup (IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** 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 CreateSystemDialogs (IAvnSystemDialogs** ppv) = 0;
virtual HRESULT CreateScreens (IAvnScreens** ppv) = 0; virtual HRESULT CreateScreens (IAvnScreens** ppv) = 0;
virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0; virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0;
virtual HRESULT CreateDndClipboard(IAvnClipboard** ppv) = 0;
virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0; virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0;
virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) = 0; virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) = 0;
virtual HRESULT SetAppMenu(IAvnMenu* menu) = 0; virtual HRESULT SetAppMenu(IAvnMenu* menu) = 0;
@ -236,6 +256,9 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown
virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0; virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0;
virtual HRESULT ObtainNSViewHandle(void** retOut) = 0; virtual HRESULT ObtainNSViewHandle(void** retOut) = 0;
virtual HRESULT ObtainNSViewHandleRetained(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 AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase
@ -245,7 +268,8 @@ AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase
AVNCOM(IAvnWindow, 04) : 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 SetCanResize(bool value) = 0;
virtual HRESULT SetDecorations(SystemDecorations value) = 0; virtual HRESULT SetDecorations(SystemDecorations value) = 0;
virtual HRESULT SetTitle (void* utf8Title) = 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 bool RawTextInputEvent (unsigned int timeStamp, const char* text) = 0;
virtual void ScalingChanged(double scaling) = 0; virtual void ScalingChanged(double scaling) = 0;
virtual void RunRenderPriorityJobs() = 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 bool Closing () = 0;
virtual void WindowStateChanged (AvnWindowState state) = 0; virtual void WindowStateChanged (AvnWindowState state) = 0;
virtual void GotInputWhenDisabled () = 0;
}; };
AVNCOM(IAvnMacOptions, 07) : IUnknown AVNCOM(IAvnMacOptions, 07) : IUnknown
@ -354,8 +383,13 @@ AVNCOM(IAvnScreens, 0e) : IUnknown
AVNCOM(IAvnClipboard, 0f) : IUnknown AVNCOM(IAvnClipboard, 0f) : IUnknown
{ {
virtual HRESULT GetText (IAvnString**ppv) = 0; virtual HRESULT GetText (char* type, IAvnString**ppv) = 0;
virtual HRESULT SetText (void* utf8Text) = 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; virtual HRESULT Clear() = 0;
}; };
@ -428,4 +462,20 @@ AVNCOM(IAvnMenuEvents, 1A) : IUnknown
virtual void NeedsUpdate () = 0; 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(); extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative();

4
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 */; }; 1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */; };
1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */; }; 1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */; };
1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */; }; 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 */; }; 37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; };
37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; }; 37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; };
37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.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; }; 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 = "<group>"; }; 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cgl.mm; sourceTree = "<group>"; };
1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 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 = "<group>"; };
37155CE3233C00EB0034DCE9 /* menu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = menu.h; sourceTree = "<group>"; }; 37155CE3233C00EB0034DCE9 /* menu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = menu.h; sourceTree = "<group>"; };
379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = "<group>"; }; 379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = "<group>"; };
37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = "<group>"; }; 37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = "<group>"; };
@ -92,6 +94,7 @@
5BF943652167AD1D009CAE35 /* cursor.h */, 5BF943652167AD1D009CAE35 /* cursor.h */,
5B21A981216530F500CEE36E /* cursor.mm */, 5B21A981216530F500CEE36E /* cursor.mm */,
5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */, 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */,
1A465D0F246AB61600C5858B /* dnd.mm */,
AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */, AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */,
AB661C212148288600291242 /* common.h */, AB661C212148288600291242 /* common.h */,
379860FE214DA0C000CD0246 /* KeyTransform.h */, 379860FE214DA0C000CD0246 /* KeyTransform.h */,
@ -196,6 +199,7 @@
37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */, 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */,
520624B322973F4100C4DCEF /* menu.mm in Sources */, 520624B322973F4100C4DCEF /* menu.mm in Sources */,
37A517B32159597E00FBA241 /* Screens.mm in Sources */, 37A517B32159597E00FBA241 /* Screens.mm in Sources */,
1A465D10246AB61600C5858B /* dnd.mm in Sources */,
AB00E4F72147CA920032A60A /* main.mm in Sources */, AB00E4F72147CA920032A60A /* main.mm in Sources */,
37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */, 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */,
AB661C202148286E00291242 /* window.mm in Sources */, AB661C202148286E00291242 /* window.mm in Sources */,

4
native/Avalonia.Native/src/OSX/AvnString.h

@ -10,5 +10,7 @@
#define AvnString_h #define AvnString_h
extern IAvnString* CreateAvnString(NSString* string); extern IAvnString* CreateAvnString(NSString* string);
extern IAvnStringArray* CreateAvnStringArray(NSArray<NSString*>* array);
extern IAvnStringArray* CreateAvnStringArray(NSString* string);
extern IAvnString* CreateByteArray(void* data, int len);
#endif /* AvnString_h */ #endif /* AvnString_h */

61
native/Avalonia.Native/src/OSX/AvnString.mm

@ -7,6 +7,7 @@
// //
#include "common.h" #include "common.h"
#include <vector>
class AvnStringImpl : public virtual ComSingleObject<IAvnString, &IID_IAvnString> class AvnStringImpl : public virtual ComSingleObject<IAvnString, &IID_IAvnString>
{ {
@ -28,6 +29,13 @@ public:
memcpy((void*)_cstring, (void*)cstring, _length); 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() virtual ~AvnStringImpl()
{ {
free((void*)_cstring); free((void*)_cstring);
@ -61,7 +69,60 @@ public:
} }
}; };
class AvnStringArrayImpl : public virtual ComSingleObject<IAvnStringArray, &IID_IAvnStringArray>
{
private:
std::vector<ComPtr<IAvnString>> _list;
public:
FORWARD_IUNKNOWN()
AvnStringArrayImpl(NSArray<NSString*>* array)
{
for(int c = 0; c < [array count]; c++)
{
ComPtr<IAvnString> s;
*s.getPPV() = new AvnStringImpl([array objectAtIndex:c]);
_list.push_back(s);
}
}
AvnStringArrayImpl(NSString* string)
{
ComPtr<IAvnString> 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) IAvnString* CreateAvnString(NSString* string)
{ {
return new AvnStringImpl(string); return new AvnStringImpl(string);
} }
IAvnStringArray* CreateAvnStringArray(NSArray<NSString*> * array)
{
return new AvnStringArrayImpl(array);
}
IAvnStringArray* CreateAvnStringArray(NSString* string)
{
return new AvnStringArrayImpl(string);
}
IAvnString* CreateByteArray(void* data, int len)
{
return new AvnStringImpl(data, len);
}

124
native/Avalonia.Native/src/OSX/clipboard.mm

@ -3,16 +3,27 @@
class Clipboard : public ComSingleObject<IAvnClipboard, &IID_IAvnClipboard> class Clipboard : public ComSingleObject<IAvnClipboard, &IID_IAvnClipboard>
{ {
private:
NSPasteboard* _pb;
NSPasteboardItem* _item;
public: public:
FORWARD_IUNKNOWN() FORWARD_IUNKNOWN()
Clipboard() Clipboard(NSPasteboard* pasteboard, NSPasteboardItem* item)
{ {
NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; if(pasteboard == nil && item == nil)
[pasteBoard stringForType:NSPasteboardTypeString]; 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 @autoreleasepool
{ {
@ -20,39 +31,124 @@ public:
{ {
return E_POINTER; 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; return S_OK;
} }
} }
virtual HRESULT SetText (void* utf8String) override virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) override
{ {
@autoreleasepool @autoreleasepool
{ {
NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; *ppv= nil;
[pasteBoard clearContents]; NSString* typeString = [NSString stringWithUTF8String:(const char*)type];
[pasteBoard setString:[NSString stringWithUTF8String:(const char*)utf8String] forType:NSPasteboardTypeString]; 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; 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 virtual HRESULT Clear() override
{ {
@autoreleasepool @autoreleasepool
{ {
NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; if(_item != nil)
[pasteBoard clearContents]; _item = [NSPasteboardItem new];
[pasteBoard setString:@"" forType:NSPasteboardTypeString]; else
{
[_pb clearContents];
[_pb setString:@"" forType:NSPasteboardTypeString];
}
} }
return S_OK; return S_OK;
} }
virtual HRESULT ObtainFormats(IAvnStringArray** ppv) override
{
*ppv = CreateAvnStringArray(_item == nil ? [_pb types] : [_item types]);
return S_OK;
}
}; };
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<Clipboard*>(cb);
if(clipboard == nil)
return nil;
return clipboard->TryGetItem();
} }

8
native/Avalonia.Native/src/OSX/common.h

@ -8,11 +8,17 @@
#include <pthread.h> #include <pthread.h>
extern IAvnPlatformThreadingInterface* CreatePlatformThreading(); extern IAvnPlatformThreadingInterface* CreatePlatformThreading();
extern void FreeAvnGCHandle(void* handle);
extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl); extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl);
extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl); extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl);
extern IAvnSystemDialogs* CreateSystemDialogs(); extern IAvnSystemDialogs* CreateSystemDialogs();
extern IAvnScreens* CreateScreens(); extern IAvnScreens* CreateScreens();
extern IAvnClipboard* CreateClipboard(); extern IAvnClipboard* CreateClipboard(NSPasteboard*, NSPasteboardItem*);
extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*);
extern NSObject<NSDraggingSource>* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle);
extern void* GetAvnDataObjectHandleFromDraggingInfo(NSObject<NSDraggingInfo>* info);
extern NSString* GetAvnCustomDataType();
extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop);
extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnCursorFactory* CreateCursorFactory();
extern IAvnGlDisplay* GetGlDisplay(); extern IAvnGlDisplay* GetGlDisplay();
extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events); extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events);

89
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<NSDraggingSource>
@end
@implementation AvnDndSource
{
NSDragOperation _operation;
ComPtr<IAvnDndResultCallback> _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<NSDraggingSource>* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle)
{
return [[AvnDndSource alloc] initWithOperation:op andCallback:cb andSourceHandle:handle];
};
extern void* GetAvnDataObjectHandleFromDraggingInfo(NSObject<NSDraggingInfo>* info)
{
id obj = [info draggingSource];
if(obj == nil)
return nil;
if([obj isKindOfClass: [AvnDndSource class]])
{
auto src = (AvnDndSource*)obj;
return [src gcHandle];
}
return nil;
}

19
native/Avalonia.Native/src/OSX/main.mm

@ -150,14 +150,15 @@ public:
} }
@end @end
static ComPtr<IAvnGCHandleDeallocatorCallback> _deallocator;
class AvaloniaNative : public ComSingleObject<IAvaloniaNativeFactory, &IID_IAvaloniaNativeFactory> class AvaloniaNative : public ComSingleObject<IAvaloniaNativeFactory, &IID_IAvaloniaNativeFactory>
{ {
public: public:
FORWARD_IUNKNOWN() FORWARD_IUNKNOWN()
virtual HRESULT Initialize() override virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) override
{ {
_deallocator = deallocator;
@autoreleasepool{ @autoreleasepool{
[[ThreadingInitializer new] do]; [[ThreadingInitializer new] do];
} }
@ -207,7 +208,13 @@ public:
virtual HRESULT CreateClipboard(IAvnClipboard** ppv) override 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; return S_OK;
} }
@ -257,6 +264,12 @@ extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative()
return new AvaloniaNative(); return new AvaloniaNative();
}; };
extern void FreeAvnGCHandle(void* handle)
{
if(_deallocator != nil)
_deallocator->FreeGCHandle(handle);
}
NSSize ToNSSize (AvnSize s) NSSize ToNSSize (AvnSize s)
{ {
NSSize result; NSSize result;

8
native/Avalonia.Native/src/OSX/window.h

@ -3,7 +3,10 @@
class WindowBaseImpl; class WindowBaseImpl;
@interface AvnView : NSView<NSTextInputClient> @interface AutoFitContentVisualEffectView : NSVisualEffectView
@end
@interface AvnView : NSView<NSTextInputClient, NSDraggingDestination>
-(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent;
-(NSEvent* _Nonnull) lastMouseDownEvent; -(NSEvent* _Nonnull) lastMouseDownEvent;
-(AvnPoint) translateLocalPoint:(AvnPoint)pt; -(AvnPoint) translateLocalPoint:(AvnPoint)pt;
@ -19,8 +22,7 @@ class WindowBaseImpl;
-(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) pollModalSession: (NSModalSession _Nonnull) session;
-(void) restoreParentWindow; -(void) restoreParentWindow;
-(bool) shouldTryToHandleEvents; -(bool) shouldTryToHandleEvents;
-(bool) isModal; -(void) setEnabled: (bool) enable;
-(void) setModal: (bool) isModal;
-(void) showAppMenuOnly; -(void) showAppMenuOnly;
-(void) showWindowMenuWithAppMenu; -(void) showWindowMenuWithAppMenu;
-(void) applyMenu:(NSMenu* _Nullable)menu; -(void) applyMenu:(NSMenu* _Nullable)menu;

211
native/Avalonia.Native/src/OSX/window.mm

@ -20,6 +20,7 @@ public:
View = NULL; View = NULL;
Window = NULL; Window = NULL;
} }
NSVisualEffectView* VisualEffect;
AvnView* View; AvnView* View;
AvnWindow* Window; AvnWindow* Window;
ComPtr<IAvnWindowBaseEvents> BaseEvents; ComPtr<IAvnWindowBaseEvents> BaseEvents;
@ -47,6 +48,12 @@ public:
[Window setStyleMask:NSWindowStyleMaskBorderless]; [Window setStyleMask:NSWindowStyleMaskBorderless];
[Window setBackingType:NSBackingStoreBuffered]; [Window setBackingType:NSBackingStoreBuffered];
VisualEffect = [AutoFitContentVisualEffectView new];
[VisualEffect setBlendingMode:NSVisualEffectBlendingModeBehindWindow];
[VisualEffect setMaterial:NSVisualEffectMaterialLight];
[VisualEffect setAutoresizesSubviews:true];
[Window setContentView: View]; [Window setContentView: View];
} }
@ -382,6 +389,62 @@ public:
*ppv = [renderTarget createSurfaceRenderTarget]; *ppv = [renderTarget createSurfaceRenderTarget];
return *ppv == nil ? E_FAIL : S_OK; 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: protected:
virtual NSWindowStyleMask GetStyle() virtual NSWindowStyleMask GetStyle()
@ -453,12 +516,7 @@ private:
virtual HRESULT Show () override virtual HRESULT Show () override
{ {
@autoreleasepool @autoreleasepool
{ {
if([Window parentWindow] != nil)
[[Window parentWindow] removeChildWindow:Window];
[Window setModal:FALSE];
WindowBaseImpl::Show(); WindowBaseImpl::Show();
HideOrShowTrafficLights(); 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 @autoreleasepool
{ {
@ -478,12 +545,9 @@ private:
if(cparent == nullptr) if(cparent == nullptr)
return E_INVALIDARG; return E_INVALIDARG;
[Window setModal:TRUE];
[cparent->Window addChildWindow:Window ordered:NSWindowAbove]; [cparent->Window addChildWindow:Window ordered:NSWindowAbove];
WindowBaseImpl::Show();
HideOrShowTrafficLights(); UpdateStyle();
return S_OK; return S_OK;
} }
@ -839,15 +903,15 @@ protected:
switch (_decorations) switch (_decorations)
{ {
case SystemDecorationsNone: case SystemDecorationsNone:
s = s | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable; s = s | NSWindowStyleMaskFullSizeContentView;
break; break;
case SystemDecorationsBorderOnly: case SystemDecorationsBorderOnly:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable; s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView;
break; break;
case SystemDecorationsFull: case SystemDecorationsFull:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless; s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskBorderless;
if(_canResize) if(_canResize)
{ {
@ -856,12 +920,26 @@ protected:
break; break;
} }
if([Window parentWindow] == nullptr)
{
s |= NSWindowStyleMaskMiniaturizable;
}
return s; return s;
} }
}; };
NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil]; 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 @implementation AvnView
{ {
ComPtr<WindowBaseImpl> _parent; ComPtr<WindowBaseImpl> _parent;
@ -911,7 +989,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_area = nullptr; _area = nullptr;
_lastPixelSize.Height = 100; _lastPixelSize.Height = 100;
_lastPixelSize.Width = 100; _lastPixelSize.Width = 100;
[self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]];
return self; return self;
} }
@ -1037,15 +1115,28 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
- (bool) ignoreUserInput - (bool) ignoreUserInput
{ {
auto parentWindow = objc_cast<AvnWindow>([self window]); auto parentWindow = objc_cast<AvnWindow>([self window]);
if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents]) if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents])
{
auto window = dynamic_cast<WindowImpl*>(_parent.getRaw());
if(window != nullptr)
{
window->WindowEvents->GotInputWhenDisabled();
}
return TRUE; return TRUE;
}
return FALSE; return FALSE;
} }
- (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type - (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type
{ {
if([self ignoreUserInput]) if([self ignoreUserInput])
{
return; return;
}
[self becomeFirstResponder]; [self becomeFirstResponder];
auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; 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 - (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type
{ {
if([self ignoreUserInput]) if([self ignoreUserInput])
{
return; return;
}
auto key = s_KeyMap[[event keyCode]]; auto key = s_KeyMap[[event keyCode]];
auto timestamp = [event timestamp] * 1000; auto timestamp = [event timestamp] * 1000;
@ -1302,6 +1396,68 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
return result; return result;
} }
- (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id <NSDraggingInfo>)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 <NSDraggingInfo>)sender
{
return [self triggerAvnDragEvent: AvnDragEventType::Enter info:sender];
}
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
{
return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender];
}
- (void)draggingExited:(id <NSDraggingInfo>)sender
{
[self triggerAvnDragEvent: AvnDragEventType::Leave info:sender];
}
- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
{
return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender] != NSDragOperationNone;
}
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
return [self triggerAvnDragEvent: AvnDragEventType::Drop info:sender] != NSDragOperationNone;
}
- (void)concludeDragOperation:(nullable id <NSDraggingInfo>)sender
{
}
@end @end
@ -1310,7 +1466,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
ComPtr<WindowBaseImpl> _parent; ComPtr<WindowBaseImpl> _parent;
bool _canBecomeKeyAndMain; bool _canBecomeKeyAndMain;
bool _closed; bool _closed;
bool _isModal; bool _isEnabled;
AvnMenu* _menu; AvnMenu* _menu;
double _lastScaling; double _lastScaling;
} }
@ -1432,6 +1588,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_parent = parent; _parent = parent;
[self setDelegate:self]; [self setDelegate:self];
_closed = false; _closed = false;
_isEnabled = true;
_lastScaling = [self backingScaleFactor]; _lastScaling = [self backingScaleFactor];
[self setOpaque:NO]; [self setOpaque:NO];
@ -1498,28 +1655,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
-(bool)shouldTryToHandleEvents -(bool)shouldTryToHandleEvents
{ {
for(NSWindow* uch in [self childWindows]) return _isEnabled;
{
auto ch = objc_cast<AvnWindow>(uch);
if(ch == nil)
continue;
if(![ch isModal])
continue;
return FALSE;
}
return TRUE;
}
-(bool) isModal
{
return _isModal;
} }
-(void) setModal: (bool) isModal -(void) setEnabled:(bool)enable
{ {
_isModal = isModal; _isEnabled = enable;
} }
-(void)makeKeyWindow -(void)makeKeyWindow

5
packages/Avalonia/AvaloniaBuildTasks.targets

@ -2,6 +2,7 @@
<PropertyGroup> <PropertyGroup>
<_AvaloniaUseExternalMSBuild>$(AvaloniaUseExternalMSBuild)</_AvaloniaUseExternalMSBuild> <_AvaloniaUseExternalMSBuild>$(AvaloniaUseExternalMSBuild)</_AvaloniaUseExternalMSBuild>
<_AvaloniaUseExternalMSBuild Condition="'$(_AvaloniaForceInternalMSBuild)' == 'true'">false</_AvaloniaUseExternalMSBuild> <_AvaloniaUseExternalMSBuild Condition="'$(_AvaloniaForceInternalMSBuild)' == 'true'">false</_AvaloniaUseExternalMSBuild>
<AvaloniaXamlReportImportance Condition="'$(AvaloniaXamlReportImportance)' == ''">low</AvaloniaXamlReportImportance>
</PropertyGroup> </PropertyGroup>
<UsingTask TaskName="GenerateAvaloniaResourcesTask" <UsingTask TaskName="GenerateAvaloniaResourcesTask"
@ -38,7 +39,8 @@
Output="$(AvaloniaResourcesTemporaryFilePath)" Output="$(AvaloniaResourcesTemporaryFilePath)"
Root="$(MSBuildProjectDirectory)" Root="$(MSBuildProjectDirectory)"
Resources="@(AvaloniaResource)" Resources="@(AvaloniaResource)"
EmbeddedResources="@(EmbeddedResources)"/> EmbeddedResources="@(EmbeddedResources)"
ReportImportance="$(AvaloniaXamlReportImportance)"/>
<Exec <Exec
Condition="'$(_AvaloniaUseExternalMSBuild)' == 'true'" Condition="'$(_AvaloniaUseExternalMSBuild)' == 'true'"
Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:GenerateAvaloniaResources /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false"/> Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:GenerateAvaloniaResources /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false"/>
@ -67,6 +69,7 @@
OriginalCopyPath="$(AvaloniaXamlOriginalCopyFilePath)" OriginalCopyPath="$(AvaloniaXamlOriginalCopyFilePath)"
ProjectDirectory="$(MSBuildProjectDirectory)" ProjectDirectory="$(MSBuildProjectDirectory)"
VerifyIl="$(AvaloniaXamlIlVerifyIl)" VerifyIl="$(AvaloniaXamlIlVerifyIl)"
ReportImportance="$(AvaloniaXamlReportImportance)"
/> />
<Exec <Exec
Condition="'$(_AvaloniaUseExternalMSBuild)' == 'true'" Condition="'$(_AvaloniaUseExternalMSBuild)' == 'true'"

33
readme.md

@ -1,24 +1,20 @@
<img src='https://avatars2.githubusercontent.com/u/14075148?s=200&v=4' width='100' /> [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg)
<br />
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) [![MyGet](https://img.shields.io/myget/avalonia-ci/vpre/Avalonia.svg?label=myget)](https://www.myget.org/gallery/avalonia-ci) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg)
# Avalonia <img alt="Avalonia" src="https://user-images.githubusercontent.com/6759207/84897744-cab6d800-b0ae-11ea-8214-e5174d71f5c8.png" width="400"/>
| Gitter Chat | Build Status (Win, Linux, OSX) | Open Collective | NuGet | MyGet | ## 📖 About AvaloniaUI
|---|---|---|---|---|
| [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) | [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) | [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) | [![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) | [![MyGet](https://img.shields.io/myget/avalonia-ci/vpre/Avalonia.svg?label=myget)](https://www.myget.org/gallery/avalonia-ci) |
## About Avalonia is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows via .NET Framework and .NET Core, Linux via Xorg, macOS. Avalonia is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development.
**Avalonia** is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS. <img src="https://user-images.githubusercontent.com/6759207/84751662-7c79da00-afc5-11ea-8780-dda28db70b76.png" width="700" />
**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development. > **Note:** The UI theme you see in the picture above is still work-in-progress and will be available in the upcoming Avalonia 0.10.0 release. However, you can connect to our nightly build feed and install latest pre-release versions of Avalonia NuGet packages, if you are willing to help out with the development and testing. See [Using nightly build feed](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed) for more info.
To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239). To see the status of some of our features, please see our [Roadmap](https://github.com/AvaloniaUI/Avalonia/issues/2239). You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been. [Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia!
You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been. ## 🚀 Getting Started
[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia!
## Getting Started
The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starer guide see our [documentation](http://avaloniaui.net/docs/quickstart/create-new-project). The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starer guide see our [documentation](http://avaloniaui.net/docs/quickstart/create-new-project).
@ -30,6 +26,15 @@ Install-Package Avalonia
Install-Package Avalonia.Desktop Install-Package Avalonia.Desktop
``` ```
## Showcase
Examples of UIs built with Avalonia
![image](https://user-images.githubusercontent.com/4672627/84707589-5b69a880-af35-11ea-87a6-7ad57a31d314.png)
![image](https://user-images.githubusercontent.com/4672627/84708576-28281900-af37-11ea-8c88-e29dfcfa0558.png)
![image](https://user-images.githubusercontent.com/4672627/84708947-c3b98980-af37-11ea-8c9d-503334615bbf.png)
## JetBrains Rider ## JetBrains Rider
If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature, so only **YOU** can make it happen. If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature, so only **YOU** can make it happen.

4
samples/ControlCatalog/App.xaml

@ -1,9 +1,7 @@
<Application xmlns="https://github.com/avaloniaui" <Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.App"> x:Class="ControlCatalog.App">
<Application.Styles> <Application.Styles>
<StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Default.xaml"/> <StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Default.xaml"/>
<Style Selector="TextBlock.h1"> <Style Selector="TextBlock.h1">
<Setter Property="FontSize" Value="{DynamicResource FontSizeLarge}"/> <Setter Property="FontSize" Value="{DynamicResource FontSizeLarge}"/>

53
samples/ControlCatalog/App.xaml.cs

@ -1,14 +1,67 @@
using System;
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Styling;
namespace ControlCatalog namespace ControlCatalog
{ {
public class App : Application public class App : Application
{ {
public static Styles FluentDark = new Styles
{
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseDark.xaml?assembly=Avalonia.Themes.Default")
},
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("resm:Avalonia.Themes.Fluent.Accents.FluentDark.xaml?assembly=Avalonia.Themes.Fluent")
},
};
public static Styles FluentLight = new Styles
{
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default")
},
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("resm:Avalonia.Themes.Fluent.Accents.FluentLight.xaml?assembly=Avalonia.Themes.Fluent")
},
};
public static Styles DefaultLight = new Styles
{
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default")
},
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default")
},
};
public static Styles DefaultDark = new Styles
{
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseDark.xaml?assembly=Avalonia.Themes.Default")
},
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default")
},
};
public override void Initialize() public override void Initialize()
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
Styles.Insert(0, FluentDark);
} }
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()

17
samples/ControlCatalog/MainView.xaml

@ -2,7 +2,7 @@
xmlns:pages="clr-namespace:ControlCatalog.Pages" xmlns:pages="clr-namespace:ControlCatalog.Pages"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.MainView" x:Class="ControlCatalog.MainView"
Background="{DynamicResource ThemeBackgroundBrush}" Background="Transparent"
Foreground="{DynamicResource ThemeForegroundBrush}" Foreground="{DynamicResource ThemeForegroundBrush}"
FontSize="{DynamicResource FontSizeNormal}"> FontSize="{DynamicResource FontSizeNormal}">
<Grid> <Grid>
@ -29,7 +29,8 @@
ScrollViewer.HorizontalScrollBarVisibility="Disabled"> ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<pages:DataGridPage/> <pages:DataGridPage/>
</TabItem> </TabItem>
<TabItem Header="DatePicker"><pages:DatePickerPage/></TabItem> <TabItem Header="CalendarDatePicker">
<pages:CalendarDatePickerPage/></TabItem>
<TabItem Header="Drag+Drop"><pages:DragAndDropPage/></TabItem> <TabItem Header="Drag+Drop"><pages:DragAndDropPage/></TabItem>
<TabItem Header="Expander"><pages:ExpanderPage/></TabItem> <TabItem Header="Expander"><pages:ExpanderPage/></TabItem>
<TabItem Header="Image" <TabItem Header="Image"
@ -67,8 +68,16 @@
<ComboBoxItem>Full Decorations</ComboBoxItem> <ComboBoxItem>Full Decorations</ComboBoxItem>
</ComboBox> </ComboBox>
<ComboBox x:Name="Themes" SelectedIndex="0"> <ComboBox x:Name="Themes" SelectedIndex="0">
<ComboBoxItem>Light</ComboBoxItem> <ComboBoxItem>Fluent - Dark</ComboBoxItem>
<ComboBoxItem>Dark</ComboBoxItem> <ComboBoxItem>Fluent - Light</ComboBoxItem>
<ComboBoxItem>Simple - Light</ComboBoxItem>
<ComboBoxItem>Simple - Dark</ComboBoxItem>
</ComboBox>
<ComboBox x:Name="TransparencyLevels" SelectedIndex="0">
<ComboBoxItem>None</ComboBoxItem>
<ComboBoxItem>Transparent</ComboBoxItem>
<ComboBoxItem>Blur</ComboBoxItem>
<ComboBoxItem>AcrylicBlur</ComboBoxItem>
</ComboBox> </ComboBox>
<ComboBox Items="{Binding WindowStates}" SelectedItem="{Binding WindowState}" /> <ComboBox Items="{Binding WindowStates}" SelectedItem="{Binding WindowState}" />
</StackPanel> </StackPanel>

30
samples/ControlCatalog/MainView.xaml.cs

@ -32,30 +32,25 @@ namespace ControlCatalog
} }
var light = new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default")
};
var dark = new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseDark.xaml?assembly=Avalonia.Themes.Default")
};
var themes = this.Find<ComboBox>("Themes"); var themes = this.Find<ComboBox>("Themes");
themes.SelectionChanged += (sender, e) => themes.SelectionChanged += (sender, e) =>
{ {
switch (themes.SelectedIndex) switch (themes.SelectedIndex)
{ {
case 0: case 0:
Styles[0] = light; Application.Current.Styles[0] = App.FluentDark;
break; break;
case 1: case 1:
Styles[0] = dark; Application.Current.Styles[0] = App.FluentLight;
break;
case 2:
Application.Current.Styles[0] = App.DefaultLight;
break;
case 3:
Application.Current.Styles[0] = App.DefaultDark;
break; break;
} }
}; };
Styles.Add(light);
var decorations = this.Find<ComboBox>("Decorations"); var decorations = this.Find<ComboBox>("Decorations");
decorations.SelectionChanged += (sender, e) => decorations.SelectionChanged += (sender, e) =>
@ -63,6 +58,13 @@ namespace ControlCatalog
if (VisualRoot is Window window) if (VisualRoot is Window window)
window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex; window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex;
}; };
var transparencyLevels = this.Find<ComboBox>("TransparencyLevels");
transparencyLevels.SelectionChanged += (sender, e) =>
{
if (VisualRoot is Window window)
window.TransparencyLevelHint = (WindowTransparencyLevel)transparencyLevels.SelectedIndex;
};
} }
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)

3
samples/ControlCatalog/MainWindow.xaml

@ -7,8 +7,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:vm="clr-namespace:ControlCatalog.ViewModels"
xmlns:v="clr-namespace:ControlCatalog.Views" xmlns:v="clr-namespace:ControlCatalog.Views"
x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}"> x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" Background="{DynamicResource SystemControlPageBackgroundAltHighBrush}">
<NativeMenu.Menu> <NativeMenu.Menu>
<NativeMenu> <NativeMenu>
<NativeMenuItem Header="File"> <NativeMenuItem Header="File">

6
samples/ControlCatalog/Pages/ButtonPage.xaml

@ -24,7 +24,11 @@
</Style> </Style>
</Button.Styles> </Button.Styles>
</Button> </Button>
</StackPanel> <RepeatButton Name="RepeatButton">
<TextBlock Name="RepeatButtonTextBlock" Text="Repeat Button: 0" />
</RepeatButton>
<ToggleButton Content="Toggle Button"/>
</StackPanel>
<StackPanel Orientation="Vertical" Spacing="8" Width="150"> <StackPanel Orientation="Vertical" Spacing="8" Width="150">
<Button BorderThickness="0">No Border</Button> <Button BorderThickness="0">No Border</Button>

20
samples/ControlCatalog/Pages/ButtonPage.xaml.cs

@ -5,5 +5,25 @@ namespace ControlCatalog.Pages
{ {
public class ButtonPage : UserControl public class ButtonPage : UserControl
{ {
private int repeatButtonClickCount = 0;
public ButtonPage()
{
InitializeComponent();
this.FindControl<RepeatButton>("RepeatButton").Click += OnRepeatButtonClick;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public void OnRepeatButtonClick(object sender, object args)
{
repeatButtonClickCount++;
var textBlock = this.FindControl<TextBlock>("RepeatButtonTextBlock");
textBlock.Text = $"Repeat Button: {repeatButtonClickCount}";
}
} }
} }

18
samples/ControlCatalog/Pages/DatePickerPage.xaml → samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml

@ -1,8 +1,8 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.DatePickerPage"> x:Class="ControlCatalog.Pages.CalendarDatePickerPage">
<StackPanel Orientation="Vertical" Spacing="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">DatePicker</TextBlock> <TextBlock Classes="h1">CalendarDatePicker</TextBlock>
<TextBlock Classes="h2">A control for selecting dates with a calendar drop-down</TextBlock> <TextBlock Classes="h2">A control for selecting dates with a calendar drop-down</TextBlock>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
@ -12,34 +12,34 @@
<StackPanel Orientation="Vertical" <StackPanel Orientation="Vertical"
Width="200"> Width="200">
<TextBlock Text="SelectedDateFormat: Short"/> <TextBlock Text="SelectedDateFormat: Short"/>
<DatePicker Name="DatePicker1" <CalendarDatePicker Name="DatePicker1"
SelectedDateFormat="Short" SelectedDateFormat="Short"
Margin="0,0,0,8"/> Margin="0,0,0,8"/>
<TextBlock Text="SelectedDateFormat: Long"/> <TextBlock Text="SelectedDateFormat: Long"/>
<DatePicker Name="DatePicker2" <CalendarDatePicker Name="DatePicker2"
SelectedDateFormat="Long" SelectedDateFormat="Long"
Margin="0,0,0,8"/> Margin="0,0,0,8"/>
<TextBlock Text="SelectedDateFormat: Custom"/> <TextBlock Text="SelectedDateFormat: Custom"/>
<DatePicker Name="DatePicker3" <CalendarDatePicker Name="DatePicker3"
SelectedDateFormat="Custom" SelectedDateFormat="Custom"
CustomDateFormatString="ddd, MMM d" CustomDateFormatString="ddd, MMM d"
Margin="0,0,0,8"/> Margin="0,0,0,8"/>
<TextBlock Text="Blackout Dates"/> <TextBlock Text="Blackout Dates"/>
<DatePicker Name="DatePicker4" <CalendarDatePicker Name="DatePicker4"
Margin="0,0,0,8"/> Margin="0,0,0,8"/>
<DatePicker Margin="0,0,0,8" <CalendarDatePicker Margin="0,0,0,8"
Watermark="Watermark"/> Watermark="Watermark"/>
<DatePicker Margin="0,0,0,8" <CalendarDatePicker Margin="0,0,0,8"
Name="DatePicker5" Name="DatePicker5"
Watermark="Floating Watermark" Watermark="Floating Watermark"
UseFloatingWatermark="True"/> UseFloatingWatermark="True"/>
<TextBlock Text="Disabled"/> <TextBlock Text="Disabled"/>
<DatePicker IsEnabled="False"/> <CalendarDatePicker IsEnabled="False"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>

14
samples/ControlCatalog/Pages/DatePickerPage.xaml.cs → samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml.cs

@ -4,17 +4,17 @@ using System;
namespace ControlCatalog.Pages namespace ControlCatalog.Pages
{ {
public class DatePickerPage : UserControl public class CalendarDatePickerPage : UserControl
{ {
public DatePickerPage() public CalendarDatePickerPage()
{ {
InitializeComponent(); InitializeComponent();
var dp1 = this.FindControl<DatePicker>("DatePicker1"); var dp1 = this.FindControl<CalendarDatePicker>("DatePicker1");
var dp2 = this.FindControl<DatePicker>("DatePicker2"); var dp2 = this.FindControl<CalendarDatePicker>("DatePicker2");
var dp3 = this.FindControl<DatePicker>("DatePicker3"); var dp3 = this.FindControl<CalendarDatePicker>("DatePicker3");
var dp4 = this.FindControl<DatePicker>("DatePicker4"); var dp4 = this.FindControl<CalendarDatePicker>("DatePicker4");
var dp5 = this.FindControl<DatePicker>("DatePicker5"); var dp5 = this.FindControl<CalendarDatePicker>("DatePicker5");
dp1.SelectedDate = DateTime.Today; dp1.SelectedDate = DateTime.Today;
dp2.SelectedDate = DateTime.Today.AddDays(10); dp2.SelectedDate = DateTime.Today.AddDays(10);

2
samples/ControlCatalog/Pages/ComboBoxPage.xaml

@ -6,7 +6,7 @@
<TextBlock Classes="h2">A drop-down list.</TextBlock> <TextBlock Classes="h2">A drop-down list.</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Spacing="8"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Spacing="8">
<ComboBox SelectedIndex="0"> <ComboBox PlaceholderText="Pick an Item">
<ComboBoxItem>Inline Items</ComboBoxItem> <ComboBoxItem>Inline Items</ComboBoxItem>
<ComboBoxItem>Inline Item 2</ComboBoxItem> <ComboBoxItem>Inline Item 2</ComboBoxItem>
<ComboBoxItem>Inline Item 3</ComboBoxItem> <ComboBoxItem>Inline Item 3</ComboBoxItem>

2
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -11,5 +11,7 @@
<Button Name="DecoratedWindowDialog">Decorated window (dialog)</Button> <Button Name="DecoratedWindowDialog">Decorated window (dialog)</Button>
<Button Name="Dialog">Dialog</Button> <Button Name="Dialog">Dialog</Button>
<Button Name="DialogNoTaskbar">Dialog (No taskbar icon)</Button> <Button Name="DialogNoTaskbar">Dialog (No taskbar icon)</Button>
<Button Name="OwnedWindow">Owned window</Button>
<Button Name="OwnedWindowNoTaskbar">Owned window (No taskbar icon)</Button>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

15
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -93,6 +93,21 @@ namespace ControlCatalog.Pages
window.ShowInTaskbar = false; window.ShowInTaskbar = false;
window.ShowDialog(GetWindow()); window.ShowDialog(GetWindow());
}; };
this.FindControl<Button>("OwnedWindow").Click += delegate
{
var window = CreateSampleWindow();
window.Show(GetWindow());
};
this.FindControl<Button>("OwnedWindowNoTaskbar").Click += delegate
{
var window = CreateSampleWindow();
window.ShowInTaskbar = false;
window.Show(GetWindow());
};
} }
private Window CreateSampleWindow() private Window CreateSampleWindow()

12
samples/ControlCatalog/Pages/DragAndDropPage.xaml

@ -9,9 +9,15 @@
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Spacing="16"> Spacing="16">
<Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16" Name="DragMe"> <StackPanel>
<TextBlock Name="DragState">Drag Me</TextBlock> <Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16" Name="DragMeText">
</Border> <TextBlock Name="DragStateText">Drag Me</TextBlock>
</Border>
<Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16" Name="DragMeCustom">
<TextBlock Name="DragStateCustom">Drag Me (custom)</TextBlock>
</Border>
</StackPanel>
<Border Background="{DynamicResource ThemeAccentBrush2}" Padding="16" <Border Background="{DynamicResource ThemeAccentBrush2}" Padding="16"
DragDrop.AllowDrop="True"> DragDrop.AllowDrop="True">
<TextBlock Name="DropState">Drop some text or files here</TextBlock> <TextBlock Name="DropState">Drop some text or files here</TextBlock>

92
samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs

@ -3,69 +3,85 @@ using Avalonia.Input;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text; using System.Text;
namespace ControlCatalog.Pages namespace ControlCatalog.Pages
{ {
public class DragAndDropPage : UserControl public class DragAndDropPage : UserControl
{ {
private TextBlock _DropState; TextBlock _DropState;
private TextBlock _DragState; private const string CustomFormat = "application/xxx-avalonia-controlcatalog-custom";
private Border _DragMe;
private int DragCount = 0;
public DragAndDropPage() public DragAndDropPage()
{ {
this.InitializeComponent(); this.InitializeComponent();
_DropState = this.Find<TextBlock>("DropState");
_DragMe.PointerPressed += DoDrag; int textCount = 0;
SetupDnd("Text", d => d.Set(DataFormats.Text,
$"Text was dragged {++textCount} times"));
AddHandler(DragDrop.DropEvent, Drop); SetupDnd("Custom", d => d.Set(CustomFormat, "Test123"));
AddHandler(DragDrop.DragOverEvent, DragOver);
} }
private async void DoDrag(object sender, Avalonia.Input.PointerPressedEventArgs e) void SetupDnd(string suffix, Action<DataObject> factory, DragDropEffects effects = DragDropEffects.Copy)
{ {
DataObject dragData = new DataObject(); var dragMe = this.Find<Border>("DragMe" + suffix);
dragData.Set(DataFormats.Text, $"You have dragged text {++DragCount} times"); var dragState = this.Find<TextBlock>("DragState"+suffix);
var result = await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Copy); async void DoDrag(object sender, Avalonia.Input.PointerPressedEventArgs e)
switch(result)
{ {
case DragDropEffects.Copy: var dragData = new DataObject();
_DragState.Text = "The text was copied"; break; factory(dragData);
case DragDropEffects.Link:
_DragState.Text = "The text was linked"; break; var result = await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Copy);
case DragDropEffects.None: switch (result)
_DragState.Text = "The drag operation was canceled"; break; {
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) void DragOver(object sender, DragEventArgs e)
{ {
// Only allow Copy or Link as Drop Operations. // Only allow Copy or Link as Drop Operations.
e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link); e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link);
// Only allow if the dragged data contains text or filenames. // Only allow if the dragged data contains text or filenames.
if (!e.Data.Contains(DataFormats.Text) && !e.Data.Contains(DataFormats.FileNames)) if (!e.Data.Contains(DataFormats.Text)
e.DragEffects = DragDropEffects.None; && !e.Data.Contains(DataFormats.FileNames)
} && !e.Data.Contains(CustomFormat))
e.DragEffects = DragDropEffects.None;
}
private void Drop(object sender, DragEventArgs e) void Drop(object sender, DragEventArgs e)
{ {
if (e.Data.Contains(DataFormats.Text)) if (e.Data.Contains(DataFormats.Text))
_DropState.Text = e.Data.GetText(); _DropState.Text = e.Data.GetText();
else if (e.Data.Contains(DataFormats.FileNames)) else if (e.Data.Contains(DataFormats.FileNames))
_DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames()); _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() private void InitializeComponent()
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
_DropState = this.Find<TextBlock>("DropState");
_DragState = this.Find<TextBlock>("DragState");
_DragMe = this.Find<Border>("DragMe");
} }
} }
} }

12
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@ -50,22 +50,22 @@
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Margin="2">Text:</TextBlock> <TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Margin="2">Text:</TextBlock>
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding #upDown.Text}" VerticalAlignment="Center" Margin="2" /> <TextBox Grid.Row="4" Grid.Column="1" Text="{Binding #upDown.Text}" VerticalAlignment="Center" Margin="2" />
</Grid> </Grid>
<Grid Grid.Row="0" Grid.Column="2" Margin="10,2,2,2" RowDefinitions="Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto, 120"> <Grid Grid.Row="0" Grid.Column="2" Margin="10,2,2,2" RowDefinitions="Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto, Auto">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Minimum:</TextBlock> <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Minimum:</TextBlock>
<NumericUpDown Grid.Row="0" Grid.Column="1" Value="{Binding #upDown.Minimum}" <NumericUpDown Grid.Row="0" Grid.Column="1" Value="{Binding #upDown.Minimum}"
CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Margin="2" Width="70" HorizontalAlignment="Center"/> CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Margin="2" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Maximum:</TextBlock> <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Maximum:</TextBlock>
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding #upDown.Maximum}" <NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding #upDown.Maximum}"
CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Margin="2" Width="70" HorizontalAlignment="Center"/> CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Margin="2" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Increment:</TextBlock> <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Increment:</TextBlock>
<NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding #upDown.Increment}" VerticalAlignment="Center" <NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding #upDown.Increment}" VerticalAlignment="Center"
Margin="2" Width="70" HorizontalAlignment="Center"/> Margin="2" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Value:</TextBlock> <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Value:</TextBlock>
<NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding #upDown.Value}" VerticalAlignment="Center" <NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding #upDown.Value}" VerticalAlignment="Center"
Margin="2" Width="70" HorizontalAlignment="Center"/> Margin="2" HorizontalAlignment="Center"/>
</Grid> </Grid>
</Grid> </Grid>
@ -73,7 +73,7 @@
<StackPanel Margin="2,10,2,2" Orientation="Horizontal" Spacing="10"> <StackPanel Margin="2,10,2,2" Orientation="Horizontal" Spacing="10">
<TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of NumericUpDown:</TextBlock> <TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of NumericUpDown:</TextBlock>
<NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5" <NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5"
CultureInfo="en-US" VerticalAlignment="Center" Width="100" CultureInfo="en-US" VerticalAlignment="Center"
Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/> Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/>
</StackPanel> </StackPanel>

2
samples/ControlCatalog/Pages/SliderPage.xaml

@ -9,12 +9,14 @@
<Slider Value="0" <Slider Value="0"
Minimum="0" Minimum="0"
Maximum="100" Maximum="100"
TickFrequency="10"
Width="300"/> Width="300"/>
<Slider Value="0" <Slider Value="0"
Minimum="0" Minimum="0"
Maximum="100" Maximum="100"
Orientation="Vertical" Orientation="Vertical"
IsSnapToTickEnabled="True" IsSnapToTickEnabled="True"
TickPlacement="Outside"
TickFrequency="10" TickFrequency="10"
Height="300"/> Height="300"/>
</StackPanel> </StackPanel>

2
samples/ControlCatalog/Pages/ToolTipPage.xaml

@ -18,7 +18,7 @@
ToolTip.Tip="This is a ToolTip"> ToolTip.Tip="This is a ToolTip">
<TextBlock>Hover Here</TextBlock> <TextBlock>Hover Here</TextBlock>
</Border> </Border>
<CheckBox Grid.Column="1" <ToggleSwitch Grid.Column="1"
Margin="5" Margin="5"
Grid.Row="0" Grid.Row="0"
IsChecked="{Binding ElementName=Border, Path=(ToolTip.IsOpen)}" IsChecked="{Binding ElementName=Border, Path=(ToolTip.IsOpen)}"

2
samples/ControlCatalog/Pages/TreeViewPage.xaml

@ -19,8 +19,8 @@
</TreeView> </TreeView>
<Button Command="{Binding AddItemCommand}">Add</Button> <Button Command="{Binding AddItemCommand}">Add</Button>
<Button Command="{Binding RemoveItemCommand}">Remove</Button> <Button Command="{Binding RemoveItemCommand}">Remove</Button>
<Button Command="{Binding SelectRandomItemCommand}">Select Random</Button>
<ComboBox SelectedIndex="{Binding SelectionMode, Mode=TwoWay}"> <ComboBox SelectedIndex="{Binding SelectionMode, Mode=TwoWay}">
<ComboBoxItem>Single</ComboBoxItem> <ComboBoxItem>Single</ComboBoxItem>

100
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.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using ReactiveUI; using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages namespace ControlCatalog.Pages
{ {
@ -12,105 +9,12 @@ namespace ControlCatalog.Pages
public TreeViewPage() public TreeViewPage()
{ {
InitializeComponent(); InitializeComponent();
DataContext = new PageViewModel(); DataContext = new TreeViewPageViewModel();
} }
private void InitializeComponent() private void InitializeComponent()
{ {
AvaloniaXamlLoader.Load(this); 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<Node> 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<Node> Items { get; }
public SelectionModel Selection { get; }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
public SelectionMode SelectionMode
{
get => _selectionMode;
set
{
Selection.ClearSelection();
this.RaiseAndSetIfChanged(ref _selectionMode, value);
}
}
}
private class Node
{
private int _counter;
private ObservableCollection<Node> _children;
public string Header { get; private set; }
public bool AreChildrenInitialized => _children != null;
public ObservableCollection<Node> Children
{
get
{
if (_children == null)
{
_children = new ObservableCollection<Node>(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++}" };
}
} }
} }

126
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<Node> Items { get; }
public SelectionModel Selection { get; }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
public ReactiveCommand<Unit, Unit> 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<Node> 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<Node> _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<Node> 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<Node> CreateChildren()
{
return new ObservableCollection<Node>(
Enumerable.Range(0, 10).Select(i => new Node(this, i)));
}
}
}
}

1
samples/Directory.Build.props

@ -1,6 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)..\src\tools\Avalonia.Designer.HostApp\bin\Debug\netcoreapp2.0\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
</PropertyGroup> </PropertyGroup>
<Import Project="..\build\SharedVersion.props" /> <Import Project="..\build\SharedVersion.props" />
</Project> </Project>

53
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<LineBoundsDemoControl>(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<double> AngleProperty =
AvaloniaProperty.Register<LineBoundsDemoControl, double>(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));
}
}
}

6
samples/RenderDemo/MainWindow.xaml

@ -29,6 +29,9 @@
<TabItem Header="Animations"> <TabItem Header="Animations">
<pages:AnimationsPage/> <pages:AnimationsPage/>
</TabItem> </TabItem>
<TabItem Header="Transitions">
<pages:TransitionsPage/>
</TabItem>
<TabItem Header="Clipping"> <TabItem Header="Clipping">
<pages:ClippingPage/> <pages:ClippingPage/>
</TabItem> </TabItem>
@ -44,6 +47,9 @@
<TabItem Header="GlyphRun"> <TabItem Header="GlyphRun">
<pages:GlyphRunPage/> <pages:GlyphRunPage/>
</TabItem> </TabItem>
<TabItem Header="LineBounds">
<pages:LineBoundsPage />
</TabItem>
</TabControl> </TabControl>
</DockPanel> </DockPanel>
</Window> </Window>

9
samples/RenderDemo/Pages/LineBoundsPage.xaml

@ -0,0 +1,9 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:controls="clr-namespace:RenderDemo.Controls"
x:Class="RenderDemo.Pages.LineBoundsPage">
<controls:LineBoundsDemoControl />
</UserControl>

19
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);
}
}
}

101
samples/RenderDemo/Pages/TransitionsPage.xaml

@ -0,0 +1,101 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="RenderDemo.Pages.TransitionsPage">
<UserControl.Styles>
<Styles>
<Styles.Resources>
<Template x:Key="Acorn">
<Path Fill="White" Stretch="Uniform"
Data="F1 M 16.6309,18.6563C 17.1309,
8.15625 29.8809,14.1563 29.8809,
14.1563C 30.8809,11.1563 34.1308,
11.4063 34.1308,11.4063C 33.5,12
34.6309,13.1563 34.6309,13.1563C
32.1309,13.1562 31.1309,14.9062
31.1309,14.9062C 41.1309,23.9062
32.6309,27.9063 32.6309,27.9062C
24.6309,24.9063 21.1309,22.1562
16.6309,18.6563 Z M 16.6309,19.9063C
21.6309,24.1563 25.1309,26.1562
31.6309,28.6562C 31.6309,28.6562
26.3809,39.1562 18.3809,36.1563C
18.3809,36.1563 18,38 16.3809,36.9063C
15,36 16.3809,34.9063 16.3809,34.9063C
16.3809,34.9063 10.1309,30.9062 16.6309,19.9063 Z"/>
</Template>
<Template x:Key="Heart">
<Path Fill="Red" Stretch="Uniform" Data="
M 272.70141,238.71731
C 206.46141,238.71731 152.70146,292.4773 152.70146,358.71731
C 152.70146,493.47282 288.63461,528.80461 381.26391,662.02535
C 468.83815,529.62199 609.82641,489.17075 609.82641,358.71731
C 609.82641,292.47731 556.06651,238.7173 489.82641,238.71731
C 441.77851,238.71731 400.42481,267.08774 381.26391,307.90481
C 362.10311,267.08773 320.74941,238.7173 272.70141,238.71731 z "/>
</Template>
</Styles.Resources>
<Style Selector="Border.Test">
<Setter Property="Margin" Value="15"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="100"/>
<Setter Property="Child" Value="{StaticResource Acorn}"/>
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:1" />
</Transitions>
</Setter>
<Setter Property="RenderTransform" Value="none" />
</Style>
<Style Selector="Border.Rect1:pointerover">
<Setter Property="RenderTransform" Value="rotate(120deg) scale(1.5)" />
</Style>
<Style Selector="Border.Rect2:pointerover">
<Setter Property="RenderTransform" Value="scale(0.8)" />
</Style>
<Style Selector="Border.Rect3">
<Setter Property="Child" Value="{StaticResource Heart}"/>
</Style>
<Style Selector="Border.Rect3:pointerover">
<Setter Property="RenderTransform" Value="rotate(1turn)" />
</Style>
<Style Selector="Border.Rect4:pointerover">
<Setter Property="RenderTransform" Value="translateY(-100px)" />
</Style>
<Style Selector="Border.Rect5:pointerover">
<Setter Property="RenderTransform" Value="skewX(-20deg)" />
</Style>
<Style Selector="Border.Rect5:pointerover">
<Setter Property="RenderTransform" Value="skewX(-20deg)" />
</Style>
</Styles>
</UserControl.Styles>
<Grid>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" ClipToBounds="False">
<StackPanel.Clock>
<Clock />
</StackPanel.Clock>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center">Hover to activate Transform Keyframe Animations.</TextBlock>
<Button Content="{Binding PlayStateText}" Command="{Binding TogglePlayState}" Click="ToggleClock" />
</StackPanel>
<WrapPanel ClipToBounds="False">
<Border Classes="Test Rect1" Background="DarkRed"/>
<Border Classes="Test Rect2" Background="Magenta"/>
<Border Classes="Test Rect3"/>
<Border Classes="Test Rect4" Background="Navy"/>
<Border Classes="Test Rect5" Background="SeaGreen"/>
</WrapPanel>
</StackPanel>
</Grid>
</UserControl>

37
samples/RenderDemo/Pages/TransitionsPage.xaml.cs

@ -0,0 +1,37 @@
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using RenderDemo.ViewModels;
namespace RenderDemo.Pages
{
public class TransitionsPage : UserControl
{
public TransitionsPage()
{
InitializeComponent();
this.DataContext = new AnimationsPageViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void ToggleClock(object sender, RoutedEventArgs args)
{
var button = sender as Button;
var clock = button.Clock;
if (clock.PlayState == PlayState.Run)
{
clock.PlayState = PlayState.Pause;
}
else if (clock.PlayState == PlayState.Pause)
{
clock.PlayState = PlayState.Run;
}
}
}
}

3
samples/RenderDemo/RenderDemo.csproj

@ -3,6 +3,9 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Include="..\..\src\Avalonia.Visuals\Rendering\SceneGraph\LineBoundsHelper.cs" Link="LineBoundsHelper.cs" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" /> <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" /> <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />

6
src/Android/Avalonia.Android/Platform/ClipboardImpl.cs

@ -43,5 +43,11 @@ namespace Avalonia.Android.Platform
return Task.FromResult<object>(null); return Task.FromResult<object>(null);
} }
public Task SetDataObjectAsync(IDataObject data) => throw new PlatformNotSupportedException();
public Task<string[]> GetFormatsAsync() => throw new PlatformNotSupportedException();
public Task<object> GetDataAsync(string format) => throw new PlatformNotSupportedException();
} }
} }

227
src/Avalonia.Animation/Animatable.cs

@ -1,10 +1,10 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Collections.Specialized;
using System.Reactive.Linq;
using Avalonia.Collections;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Animation.Animators;
#nullable enable
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
@ -13,9 +13,24 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
public class Animatable : AvaloniaObject public class Animatable : AvaloniaObject
{ {
/// <summary>
/// Defines the <see cref="Clock"/> property.
/// </summary>
public static readonly StyledProperty<IClock> ClockProperty = public static readonly StyledProperty<IClock> ClockProperty =
AvaloniaProperty.Register<Animatable, IClock>(nameof(Clock), inherits: true); AvaloniaProperty.Register<Animatable, IClock>(nameof(Clock), inherits: true);
/// <summary>
/// Defines the <see cref="Transitions"/> property.
/// </summary>
public static readonly StyledProperty<Transitions?> TransitionsProperty =
AvaloniaProperty.Register<Animatable, Transitions?>(nameof(Transitions));
private bool _transitionsEnabled = true;
private Dictionary<ITransition, TransitionState>? _transitionState;
/// <summary>
/// Gets or sets the clock which controls the animations on the control.
/// </summary>
public IClock Clock public IClock Clock
{ {
get => GetValue(ClockProperty); get => GetValue(ClockProperty);
@ -23,72 +38,196 @@ namespace Avalonia.Animation
} }
/// <summary> /// <summary>
/// Defines the <see cref="Transitions"/> property. /// Gets or sets the property transitions for the control.
/// </summary> /// </summary>
public static readonly DirectProperty<Animatable, Transitions> TransitionsProperty = public Transitions? Transitions
AvaloniaProperty.RegisterDirect<Animatable, Transitions>( {
nameof(Transitions), get => GetValue(TransitionsProperty);
o => o.Transitions, set => SetValue(TransitionsProperty, value);
(o, v) => o.Transitions = v); }
private Transitions _transitions; /// <summary>
/// Enables transitions for the control.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
protected void EnableTransitions()
{
if (!_transitionsEnabled)
{
_transitionsEnabled = true;
private Dictionary<AvaloniaProperty, IDisposable> _previousTransitions; if (Transitions is object)
{
AddTransitions(Transitions);
}
}
}
/// <summary> /// <summary>
/// Gets or sets the property transitions for the control. /// Disables transitions for the control.
/// </summary> /// </summary>
public Transitions Transitions /// <remarks>
/// 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.
/// </remarks>
protected void DisableTransitions()
{
if (_transitionsEnabled)
{
_transitionsEnabled = false;
if (Transitions is object)
{
RemoveTransitions(Transitions);
}
}
}
protected sealed override void OnPropertyChangedCore<T>(AvaloniaPropertyChangedEventArgs<T> change)
{ {
get if (change.Property == TransitionsProperty && change.IsEffectiveValueChange)
{ {
if (_transitions is null) var oldTransitions = change.OldValue.GetValueOrDefault<Transitions>();
_transitions = new Transitions(); var newTransitions = change.NewValue.GetValueOrDefault<Transitions>();
if (_previousTransitions is null) if (newTransitions is object)
_previousTransitions = new Dictionary<AvaloniaProperty, IDisposable>(); {
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) for (var i = Transitions.Count -1; i >= 0; --i)
return; {
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) if (!Equals(oldValue, newValue))
_previousTransitions = new Dictionary<AvaloniaProperty, IDisposable>(); {
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<IGlobalClock>(),
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<T>( private void AddTransitions(IList items)
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{ {
if (_transitions is null || _previousTransitions is null || priority == BindingPriority.Animation) if (!_transitionsEnabled)
{
return; return;
}
_transitionState ??= new Dictionary<ITransition, TransitionState>();
// PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations). for (var i = 0; i < items.Count; ++i)
foreach (var transition in _transitions)
{ {
if (transition.Property == property) var t = (ITransition)items[i];
_transitionState.Add(t, new TransitionState
{ {
if (_previousTransitions.TryGetValue(property, out var dispose)) BaseValue = GetAnimationBaseValue(t.Property),
dispose.Dispose(); });
}
}
var instance = transition.Apply( private void RemoveTransitions(IList items)
this, {
Clock ?? Avalonia.Animation.Clock.GlobalClock, if (_transitionState is null)
oldValue.GetValueOrDefault(), {
newValue.GetValueOrDefault()); return;
}
_previousTransitions[property] = instance; for (var i = 0; i < items.Count; ++i)
return; {
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; }
}
} }
} }

2
src/Avalonia.Animation/TransitionInstance.cs

@ -19,6 +19,8 @@ namespace Avalonia.Animation
public TransitionInstance(IClock clock, TimeSpan Duration) public TransitionInstance(IClock clock, TimeSpan Duration)
{ {
clock = clock ?? throw new ArgumentNullException(nameof(clock));
_duration = Duration; _duration = Duration;
_baseClock = clock; _baseClock = clock;
} }

13
src/Avalonia.Animation/Transitions.cs

@ -1,4 +1,6 @@
using System;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Threading;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
@ -13,6 +15,17 @@ namespace Avalonia.Animation
public Transitions() public Transitions()
{ {
ResetBehavior = ResetBehavior.Remove; 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.");
}
} }
} }
} }

204
src/Avalonia.Base/AvaloniaObject.cs

@ -259,6 +259,21 @@ namespace Avalonia
return registered.InvokeGetter(this); return registered.InvokeGetter(this);
} }
/// <inheritdoc/>
public Optional<T> GetBaseValue<T>(StyledPropertyBase<T> 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;
}
/// <summary> /// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is animating. /// Checks whether a <see cref="AvaloniaProperty"/> is animating.
/// </summary> /// </summary>
@ -458,29 +473,43 @@ namespace Avalonia
return _propertyChanged?.GetInvocationList(); return _propertyChanged?.GetInvocationList();
} }
void IValueSink.ValueChanged<T>( void IValueSink.ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
StyledPropertyBase<T> property,
BindingPriority priority,
Optional<T> oldValue,
BindingValue<T> newValue)
{ {
oldValue = oldValue.HasValue ? oldValue : GetInheritedOrDefault(property); var property = (StyledPropertyBase<T>)change.Property;
newValue = newValue.HasValue ? newValue : newValue.WithValue(GetInheritedOrDefault(property));
LogIfError(property, newValue); LogIfError(property, change.NewValue);
if (!EqualityComparer<T>.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<T>(property));
}
Logger.TryGet(LogEventLevel.Verbose)?.Log( if (change.IsEffectiveValueChange && !change.NewValue.HasValue)
LogArea.Property, {
this, change.SetNewValue(GetInheritedOrDefault(property));
"{Property} changed from {$Old} to {$Value} with priority {Priority}", }
property,
oldValue, if (!change.IsEffectiveValueChange ||
newValue, !EqualityComparer<T>.Default.Equals(change.OldValue.Value, change.NewValue.Value))
(BindingPriority)priority); {
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, IPriorityValueEntry entry,
Optional<T> oldValue) Optional<T> oldValue)
{ {
((IValueSink)this).ValueChanged(property, BindingPriority.Unset, oldValue, default); var change = new AvaloniaPropertyChangedEventArgs<T>(
this,
property,
oldValue,
default,
BindingPriority.Unset);
((IValueSink)this).ValueChanged(change);
} }
/// <summary> /// <summary>
@ -575,15 +610,20 @@ namespace Avalonia
/// <summary> /// <summary>
/// Called when a avalonia property changes on the object. /// Called when a avalonia property changes on the object.
/// </summary> /// </summary>
/// <param name="property">The property whose value has changed.</param> /// <param name="change">The property change details.</param>
/// <param name="oldValue">The old value of the property.</param> protected virtual void OnPropertyChangedCore<T>(AvaloniaPropertyChangedEventArgs<T> change)
/// <param name="newValue">The new value of the property.</param> {
/// <param name="priority">The priority of the new value.</param> if (change.IsEffectiveValueChange)
protected virtual void OnPropertyChanged<T>( {
AvaloniaProperty<T> property, OnPropertyChanged(change);
Optional<T> oldValue, }
BindingValue<T> newValue, }
BindingPriority priority)
/// <summary>
/// Called when a avalonia property changes on the object.
/// </summary>
/// <param name="change">The property change details.</param>
protected virtual void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{ {
} }
@ -600,57 +640,12 @@ namespace Avalonia
BindingValue<T> newValue, BindingValue<T> newValue,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority = BindingPriority.LocalValue)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); RaisePropertyChanged(new AvaloniaPropertyChangedEventArgs<T>(
this,
VerifyAccess(); property,
oldValue,
property.Notifying?.Invoke(this, true); newValue,
priority));
try
{
AvaloniaPropertyChangedEventArgs<T> e = null;
var hasChanged = property.HasChangedSubscriptions;
if (hasChanged || _propertyChanged != null)
{
e = new AvaloniaPropertyChangedEventArgs<T>(
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);
}
} }
/// <summary> /// <summary>
@ -689,7 +684,9 @@ namespace Avalonia
return property.GetDefaultValue(GetType()); return property.GetDefaultValue(GetType());
} }
private T GetValueOrInheritedOrDefault<T>(StyledPropertyBase<T> property) private T GetValueOrInheritedOrDefault<T>(
StyledPropertyBase<T> property,
BindingPriority maxPriority = BindingPriority.Animation)
{ {
var o = this; var o = this;
var inherits = property.Inherits; var inherits = property.Inherits;
@ -699,7 +696,7 @@ namespace Avalonia
{ {
var values = o._values; var values = o._values;
if (values?.TryGetValue(property, out value) == true) if (values?.TryGetValue(property, maxPriority, out value) == true)
{ {
return value; return value;
} }
@ -715,6 +712,51 @@ namespace Avalonia
return property.GetDefaultValue(GetType()); return property.GetDefaultValue(GetType());
} }
protected internal void RaisePropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> 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);
}
}
}
/// <summary> /// <summary>
/// Sets the value of a direct property. /// Sets the value of a direct property.
/// </summary> /// </summary>

59
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -448,6 +448,65 @@ namespace Avalonia
}; };
} }
/// <summary>
/// Gets an <see cref="AvaloniaProperty"/> base value.
/// </summary>
/// <param name="target">The object.</param>
/// <param name="property">The property.</param>
/// <param name="maxPriority">The maximum priority for the value.</param>
/// <remarks>
/// For styled properties, gets the value of the property if set on the object with a
/// priority equal or lower to <paramref name="maxPriority"/>, otherwise
/// <see cref="AvaloniaProperty.UnsetValue"/>. Note that this method does not return
/// property values that come from inherited or default values.
///
/// For direct properties returns <see cref="GetValue(IAvaloniaObject, AvaloniaProperty)"/>.
/// </remarks>
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);
}
/// <summary>
/// Gets an <see cref="AvaloniaProperty"/> base value.
/// </summary>
/// <param name="target">The object.</param>
/// <param name="property">The property.</param>
/// <param name="maxPriority">The maximum priority for the value.</param>
/// <remarks>
/// For styled properties, gets the value of the property if set on the object with a
/// priority equal or lower to <paramref name="maxPriority"/>, otherwise
/// <see cref="Optional{T}.Empty"/>. Note that this method does not return property values
/// that come from inherited or default values.
///
/// For direct properties returns
/// <see cref="IAvaloniaObject.GetValue{T}(DirectPropertyBase{T})"/>.
/// </remarks>
public static Optional<T> GetBaseValue<T>(
this IAvaloniaObject target,
AvaloniaProperty<T> 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<T> styled => target.GetBaseValue(styled, maxPriority),
DirectPropertyBase<T> direct => target.GetValue(direct),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type.")
};
}
/// <summary> /// <summary>
/// Sets a <see cref="AvaloniaProperty"/> value. /// Sets a <see cref="AvaloniaProperty"/> value.
/// </summary> /// </summary>

7
src/Avalonia.Base/AvaloniaProperty.cs

@ -496,6 +496,13 @@ namespace Avalonia
/// <param name="o">The object instance.</param> /// <param name="o">The object instance.</param>
internal abstract object RouteGetValue(IAvaloniaObject o); internal abstract object RouteGetValue(IAvaloniaObject o);
/// <summary>
/// Routes an untyped GetBaseValue call to a typed call.
/// </summary>
/// <param name="o">The object instance.</param>
/// <param name="maxPriority">The maximum priority for the value.</param>
internal abstract object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority);
/// <summary> /// <summary>
/// Routes an untyped SetValue call to a typed call. /// Routes an untyped SetValue call to a typed call.
/// </summary> /// </summary>

23
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs

@ -16,6 +16,7 @@ namespace Avalonia
{ {
Sender = sender; Sender = sender;
Priority = priority; Priority = priority;
IsEffectiveValueChange = true;
} }
/// <summary> /// <summary>
@ -35,19 +36,11 @@ namespace Avalonia
/// <summary> /// <summary>
/// Gets the old value of the property. /// Gets the old value of the property.
/// </summary> /// </summary>
/// <value>
/// The old value of the property or <see cref="AvaloniaProperty.UnsetValue"/> if the
/// property previously had no value.
/// </value>
public object? OldValue => GetOldValue(); public object? OldValue => GetOldValue();
/// <summary> /// <summary>
/// Gets the new value of the property. /// Gets the new value of the property.
/// </summary> /// </summary>
/// <value>
/// The new value of the property or <see cref="AvaloniaProperty.UnsetValue"/> if the
/// property previously had no value.
/// </value>
public object? NewValue => GetNewValue(); public object? NewValue => GetNewValue();
/// <summary> /// <summary>
@ -58,6 +51,20 @@ namespace Avalonia
/// </value> /// </value>
public BindingPriority Priority { get; private set; } public BindingPriority Priority { get; private set; }
/// <summary>
/// Gets a value indicating whether the change represents a change to the effective value of
/// the property.
/// </summary>
/// <remarks>
/// This will usually be true, except in
/// <see cref="AvaloniaObject.OnPropertyChangedCore{T}(AvaloniaPropertyChangedEventArgs{T})"/>
/// 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.
/// </remarks>
public bool IsEffectiveValueChange { get; private set; }
internal void MarkNonEffectiveValue() => IsEffectiveValueChange = false;
protected abstract AvaloniaProperty GetProperty(); protected abstract AvaloniaProperty GetProperty();
protected abstract object? GetOldValue(); protected abstract object? GetOldValue();
protected abstract object? GetNewValue(); protected abstract object? GetNewValue();

24
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs

@ -1,4 +1,3 @@
using System;
using Avalonia.Data; using Avalonia.Data;
#nullable enable #nullable enable
@ -6,7 +5,7 @@ using Avalonia.Data;
namespace Avalonia namespace Avalonia
{ {
/// <summary> /// <summary>
/// Provides information for a avalonia property change. /// Provides information for an Avalonia property change.
/// </summary> /// </summary>
public class AvaloniaPropertyChangedEventArgs<T> : AvaloniaPropertyChangedEventArgs public class AvaloniaPropertyChangedEventArgs<T> : AvaloniaPropertyChangedEventArgs
{ {
@ -42,19 +41,28 @@ namespace Avalonia
/// <summary> /// <summary>
/// Gets the old value of the property. /// Gets the old value of the property.
/// </summary> /// </summary>
/// <value> /// <remarks>
/// The old value of the property. /// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is true, returns the
/// </value> /// old value of the property on the object.
/// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is false, returns
/// <see cref="Optional{T}.Empty"/>.
/// </remarks>
public new Optional<T> OldValue { get; private set; } public new Optional<T> OldValue { get; private set; }
/// <summary> /// <summary>
/// Gets the new value of the property. /// Gets the new value of the property.
/// </summary> /// </summary>
/// <value> /// <remarks>
/// The new value of the property. /// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is true, returns the
/// </value> /// value of the property on the object.
/// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is false returns the
/// changed value, or <see cref="Optional{T}.Empty"/> if the value was removed.
/// </remarks>
public new BindingValue<T> NewValue { get; private set; } public new BindingValue<T> NewValue { get; private set; }
internal void SetOldValue(Optional<T> value) => OldValue = value;
internal void SetNewValue(BindingValue<T> value) => NewValue = value;
protected override AvaloniaProperty GetProperty() => Property; protected override AvaloniaProperty GetProperty() => Property;
protected override object? GetOldValue() => OldValue.GetValueOrDefault(AvaloniaProperty.UnsetValue); protected override object? GetOldValue() => OldValue.GetValueOrDefault(AvaloniaProperty.UnsetValue);

4
src/Avalonia.Base/Data/BindingValue.cs

@ -348,8 +348,8 @@ namespace Avalonia.Data
return new BindingValue<T>( return new BindingValue<T>(
fallbackValue.HasValue ? fallbackValue.HasValue ?
BindingValueType.DataValidationError : BindingValueType.DataValidationErrorWithFallback :
BindingValueType.DataValidationErrorWithFallback, BindingValueType.DataValidationError,
fallbackValue.HasValue ? fallbackValue.Value : default, fallbackValue.HasValue ? fallbackValue.Value : default,
e); e);
} }

2
src/Avalonia.Base/Data/Converters/BoolConverters.cs

@ -3,7 +3,7 @@ using System.Linq;
namespace Avalonia.Data.Converters namespace Avalonia.Data.Converters
{ {
/// <summary> /// <summary>
/// Provides a set of useful <see cref="IValueConverter"/>s for working with string values. /// Provides a set of useful <see cref="IValueConverter"/>s for working with bool values.
/// </summary> /// </summary>
public static class BoolConverters public static class BoolConverters
{ {

15
src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs

@ -1,7 +1,5 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Reflection; using System.Reflection;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -14,7 +12,7 @@ namespace Avalonia.Data.Core.Plugins
public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin
{ {
/// <inheritdoc/> /// <inheritdoc/>
public bool Match(object obj, string propertyName) => true; public bool Match(object obj, string propertyName) => GetPropertyWithName(obj.GetType(), propertyName) != null;
/// <summary> /// <summary>
/// Starts monitoring the value of a property on an object. /// Starts monitoring the value of a property on an object.
@ -31,7 +29,8 @@ namespace Avalonia.Data.Core.Plugins
Contract.Requires<ArgumentNullException>(propertyName != null); Contract.Requires<ArgumentNullException>(propertyName != null);
reference.TryGetTarget(out object instance); reference.TryGetTarget(out object instance);
var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(x => x.Name == propertyName);
var p = GetPropertyWithName(instance.GetType(), propertyName);
if (p != null) if (p != null)
{ {
@ -45,6 +44,14 @@ namespace Avalonia.Data.Core.Plugins
} }
} }
private static PropertyInfo GetPropertyWithName(Type type, string propertyName)
{
const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public |
BindingFlags.Static | BindingFlags.Instance;
return type.GetProperty(propertyName, bindingFlags);
}
private class Accessor : PropertyAccessorBase, IWeakSubscriber<PropertyChangedEventArgs> private class Accessor : PropertyAccessorBase, IWeakSubscriber<PropertyChangedEventArgs>
{ {
private readonly WeakReference<object> _reference; private readonly WeakReference<object> _reference;

93
src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs

@ -1,13 +1,16 @@
using System; using System;
using System.Linq; using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
namespace Avalonia.Data.Core.Plugins namespace Avalonia.Data.Core.Plugins
{ {
class MethodAccessorPlugin : IPropertyAccessorPlugin public class MethodAccessorPlugin : IPropertyAccessorPlugin
{ {
public bool Match(object obj, string methodName) private readonly Dictionary<(Type, string), MethodInfo> _methodLookup =
=> obj.GetType().GetRuntimeMethods().Any(x => x.Name == methodName); new Dictionary<(Type, string), MethodInfo>();
public bool Match(object obj, string methodName) => GetFirstMethodWithName(obj.GetType(), methodName) != null;
public IPropertyAccessor Start(WeakReference<object> reference, string methodName) public IPropertyAccessor Start(WeakReference<object> reference, string methodName)
{ {
@ -15,17 +18,22 @@ namespace Avalonia.Data.Core.Plugins
Contract.Requires<ArgumentNullException>(methodName != null); Contract.Requires<ArgumentNullException>(methodName != null);
reference.TryGetTarget(out object instance); 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 != 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 PropertyError(new BindingNotification(exception, BindingErrorType.Error));
} }
return new Accessor(reference, method); return new Accessor(reference, method, parameters);
} }
else 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 private sealed class Accessor : PropertyAccessorBase
{ {
public Accessor(WeakReference<object> reference, MethodInfo method) public Accessor(WeakReference<object> reference, MethodInfo method, ParameterInfo[] parameters)
{ {
Contract.Requires<ArgumentNullException>(reference != null); Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(method != null); Contract.Requires<ArgumentNullException>(method != null);
var paramTypes = method.GetParameters().Select(param => param.ParameterType).ToArray();
var returnType = method.ReturnType; var returnType = method.ReturnType;
bool hasReturn = returnType != typeof(void);
if (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) ParameterInfo parameter = parameters[i];
{
PropertyType = typeof(Action); paramTypes[i] = parameter.ParameterType;
} }
else
{ if (hasReturn)
PropertyType = Type.GetType($"System.Action`{paramTypes.Length}").MakeGenericType(paramTypes); {
} paramTypes[paramTypes.Length - 1] = returnType;
PropertyType = Expression.GetFuncType(paramTypes);
} }
else else
{ {
var genericTypeParameters = paramTypes.Concat(new[] { returnType }).ToArray(); PropertyType = Expression.GetActionType(paramTypes);
PropertyType = Type.GetType($"System.Func`{genericTypeParameters.Length}").MakeGenericType(genericTypeParameters);
} }
if (method.IsStatic) if (method.IsStatic)

9
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

@ -62,8 +62,13 @@ namespace Avalonia.Data.Core
if (accessor == null) if (accessor == null)
{ {
throw new NotSupportedException( reference.TryGetTarget(out object instance);
$"Could not find a matching property accessor for {PropertyName}.");
var message = $"Could not find a matching property accessor for '{PropertyName}' on '{instance}'";
var exception = new MissingMemberException(message);
accessor = new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
} }
_accessor = accessor; _accessor = accessor;

5
src/Avalonia.Base/DirectPropertyBase.cs

@ -120,6 +120,11 @@ namespace Avalonia
return o.GetValue<TValue>(this); return o.GetValue<TValue>(this);
} }
internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority)
{
return o.GetValue<TValue>(this);
}
/// <inheritdoc/> /// <inheritdoc/>
internal override IDisposable? RouteSetValue( internal override IDisposable? RouteSetValue(
IAvaloniaObject o, IAvaloniaObject o,

13
src/Avalonia.Base/IAvaloniaObject.cs

@ -41,6 +41,19 @@ namespace Avalonia
/// <returns>The value.</returns> /// <returns>The value.</returns>
T GetValue<T>(DirectPropertyBase<T> property); T GetValue<T>(DirectPropertyBase<T> property);
/// <summary>
/// Gets an <see cref="AvaloniaProperty"/> base value.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="maxPriority">The maximum priority for the value.</param>
/// <remarks>
/// Gets the value of the property, if set on this object with a priority equal or lower to
/// <paramref name="maxPriority"/>, otherwise <see cref="Optional{T}.Empty"/>. Note that
/// this method does not return property values that come from inherited or default values.
/// </remarks>
Optional<T> GetBaseValue<T>(StyledPropertyBase<T> property, BindingPriority maxPriority);
/// <summary> /// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is animating. /// Checks whether a <see cref="AvaloniaProperty"/> is animating.
/// </summary> /// </summary>

3
src/Avalonia.Base/Properties/AssemblyInfo.cs

@ -7,4 +7,5 @@ using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Data.Converters")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Data.Converters")]
[assembly: InternalsVisibleTo("Avalonia.Base.UnitTests")] [assembly: InternalsVisibleTo("Avalonia.Base.UnitTests")]
[assembly: InternalsVisibleTo("Avalonia.UnitTests")] [assembly: InternalsVisibleTo("Avalonia.UnitTests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid")]

20
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@ -22,6 +22,7 @@ namespace Avalonia.PropertyStore
private readonly IAvaloniaObject _owner; private readonly IAvaloniaObject _owner;
private IValueSink _sink; private IValueSink _sink;
private IDisposable? _subscription; private IDisposable? _subscription;
private Optional<T> _value;
public BindingEntry( public BindingEntry(
IAvaloniaObject owner, IAvaloniaObject owner,
@ -40,18 +41,21 @@ namespace Avalonia.PropertyStore
public StyledPropertyBase<T> Property { get; } public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; } public BindingPriority Priority { get; }
public IObservable<BindingValue<T>> Source { get; } public IObservable<BindingValue<T>> Source { get; }
public Optional<T> Value { get; private set; } Optional<object> IValue.GetValue() => _value.ToObject();
Optional<object> IValue.Value => Value.ToObject();
BindingPriority IValue.ValuePriority => Priority; public Optional<T> GetValue(BindingPriority maxPriority)
{
return Priority >= maxPriority ? _value : Optional<T>.Empty;
}
public void Dispose() public void Dispose()
{ {
_subscription?.Dispose(); _subscription?.Dispose();
_subscription = null; _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) public void OnError(Exception error)
{ {
@ -94,14 +98,14 @@ namespace Avalonia.PropertyStore
return; return;
} }
var old = Value; var old = _value;
if (value.Type != BindingValueType.DataValidationError) if (value.Type != BindingValueType.DataValidationError)
{ {
Value = value.ToOptional(); _value = value.ToOptional();
} }
_sink.ValueChanged(Property, Priority, old, value); _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(_owner, Property, old, value, Priority));
} }
} }
} }

14
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@ -13,6 +13,7 @@ namespace Avalonia.PropertyStore
internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IDisposable internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IDisposable
{ {
private IValueSink _sink; private IValueSink _sink;
private Optional<T> _value;
public ConstantValueEntry( public ConstantValueEntry(
StyledPropertyBase<T> property, StyledPropertyBase<T> property,
@ -21,18 +22,21 @@ namespace Avalonia.PropertyStore
IValueSink sink) IValueSink sink)
{ {
Property = property; Property = property;
Value = value; _value = value;
Priority = priority; Priority = priority;
_sink = sink; _sink = sink;
} }
public StyledPropertyBase<T> Property { get; } public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; } public BindingPriority Priority { get; }
public Optional<T> Value { get; } Optional<object> IValue.GetValue() => _value.ToObject();
Optional<object> IValue.Value => Value.ToObject();
BindingPriority IValue.ValuePriority => Priority;
public void Dispose() => _sink.Completed(Property, this, Value); public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation)
{
return Priority >= maxPriority ? _value : Optional<T>.Empty;
}
public void Dispose() => _sink.Completed(Property, this, _value);
public void Reparent(IValueSink sink) => _sink = sink; public void Reparent(IValueSink sink) => _sink = sink;
} }
} }

6
src/Avalonia.Base/PropertyStore/IValue.cs

@ -9,8 +9,8 @@ namespace Avalonia.PropertyStore
/// </summary> /// </summary>
internal interface IValue internal interface IValue
{ {
Optional<object> Value { get; } Optional<object> GetValue();
BindingPriority ValuePriority { get; } BindingPriority Priority { get; }
} }
/// <summary> /// <summary>
@ -19,6 +19,6 @@ namespace Avalonia.PropertyStore
/// <typeparam name="T">The property type.</typeparam> /// <typeparam name="T">The property type.</typeparam>
internal interface IValue<T> : IValue internal interface IValue<T> : IValue
{ {
new Optional<T> Value { get; } Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation);
} }
} }

6
src/Avalonia.Base/PropertyStore/IValueSink.cs

@ -9,11 +9,7 @@ namespace Avalonia.PropertyStore
/// </summary> /// </summary>
internal interface IValueSink internal interface IValueSink
{ {
void ValueChanged<T>( void ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change);
StyledPropertyBase<T> property,
BindingPriority priority,
Optional<T> oldValue,
BindingValue<T> newValue);
void Completed<T>( void Completed<T>(
StyledPropertyBase<T> property, StyledPropertyBase<T> property,

11
src/Avalonia.Base/PropertyStore/LocalValueEntry.cs

@ -14,9 +14,14 @@ namespace Avalonia.PropertyStore
private T _value; private T _value;
public LocalValueEntry(T value) => _value = value; public LocalValueEntry(T value) => _value = value;
public Optional<T> Value => _value; public BindingPriority Priority => BindingPriority.LocalValue;
public BindingPriority ValuePriority => BindingPriority.LocalValue; Optional<object> IValue.GetValue() => new Optional<object>(_value);
Optional<object> IValue.Value => Value.ToObject();
public Optional<T> GetValue(BindingPriority maxPriority)
{
return BindingPriority.LocalValue >= maxPriority ? _value : Optional<T>.Empty;
}
public void SetValue(T value) => _value = value; public void SetValue(T value) => _value = value;
} }
} }

161
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Data; using Avalonia.Data;
#nullable enable #nullable enable
@ -24,6 +25,7 @@ namespace Avalonia.PropertyStore
private readonly List<IPriorityValueEntry<T>> _entries = new List<IPriorityValueEntry<T>>(); private readonly List<IPriorityValueEntry<T>> _entries = new List<IPriorityValueEntry<T>>();
private readonly Func<IAvaloniaObject, T, T>? _coerceValue; private readonly Func<IAvaloniaObject, T, T>? _coerceValue;
private Optional<T> _localValue; private Optional<T> _localValue;
private Optional<T> _value;
public PriorityValue( public PriorityValue(
IAvaloniaObject owner, IAvaloniaObject owner,
@ -50,11 +52,13 @@ namespace Avalonia.PropertyStore
{ {
existing.Reparent(this); existing.Reparent(this);
_entries.Add(existing); _entries.Add(existing);
var v = existing.GetValue();
if (existing.Value.HasValue) if (v.HasValue)
{ {
Value = existing.Value; _value = v;
ValuePriority = existing.Priority; Priority = existing.Priority;
} }
} }
@ -65,18 +69,39 @@ namespace Avalonia.PropertyStore
LocalValueEntry<T> existing) LocalValueEntry<T> existing)
: this(owner, property, sink) : this(owner, property, sink)
{ {
_localValue = existing.Value; _value = _localValue = existing.GetValue(BindingPriority.LocalValue);
Value = _localValue; Priority = BindingPriority.LocalValue;
ValuePriority = BindingPriority.LocalValue;
} }
public StyledPropertyBase<T> Property { get; } public StyledPropertyBase<T> Property { get; }
public Optional<T> Value { get; private set; } public BindingPriority Priority { get; private set; } = BindingPriority.Unset;
public BindingPriority ValuePriority { get; private set; }
public IReadOnlyList<IPriorityValueEntry<T>> Entries => _entries; public IReadOnlyList<IPriorityValueEntry<T>> Entries => _entries;
Optional<object> IValue.Value => Value.ToObject(); Optional<object> IValue.GetValue() => _value.ToObject();
public void ClearLocalValue()
{
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
Property,
default,
default,
BindingPriority.LocalValue));
}
public Optional<T> 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) public IDisposable? SetValue(T value, BindingPriority priority)
{ {
@ -94,7 +119,13 @@ namespace Avalonia.PropertyStore
result = entry; result = entry;
} }
UpdateEffectiveValue(); UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
Property,
default,
value,
priority));
return result; return result;
} }
@ -106,20 +137,19 @@ namespace Avalonia.PropertyStore
return binding; return binding;
} }
public void CoerceValue() => UpdateEffectiveValue(); public void CoerceValue() => UpdateEffectiveValue(null);
void IValueSink.ValueChanged<TValue>( void IValueSink.ValueChanged<TValue>(AvaloniaPropertyChangedEventArgs<TValue> change)
StyledPropertyBase<TValue> property,
BindingPriority priority,
Optional<TValue> oldValue,
BindingValue<TValue> newValue)
{ {
if (priority == BindingPriority.LocalValue) if (change.Priority == BindingPriority.LocalValue)
{ {
_localValue = default; _localValue = default;
} }
UpdateEffectiveValue(); if (change is AvaloniaPropertyChangedEventArgs<T> c)
{
UpdateEffectiveValue(c);
}
} }
void IValueSink.Completed<TValue>( void IValueSink.Completed<TValue>(
@ -128,7 +158,16 @@ namespace Avalonia.PropertyStore
Optional<TValue> oldValue) Optional<TValue> oldValue)
{ {
_entries.Remove((IPriorityValueEntry<T>)entry); _entries.Remove((IPriorityValueEntry<T>)entry);
UpdateEffectiveValue();
if (oldValue is Optional<T> o)
{
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
Property,
o,
default,
entry.Priority));
}
} }
private int FindInsertPoint(BindingPriority priority) private int FindInsertPoint(BindingPriority priority)
@ -147,53 +186,73 @@ namespace Avalonia.PropertyStore
return result; return result;
} }
private void UpdateEffectiveValue() public (Optional<T>, BindingPriority) CalculateValue(BindingPriority maxPriority)
{ {
var reachedLocalValues = false; var reachedLocalValues = false;
var value = default(Optional<T>);
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]; return (entryValue, entry.Priority);
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;
}
} }
} }
else if (_localValue.HasValue)
if (!reachedLocalValues &&
maxPriority <= BindingPriority.LocalValue &&
_localValue.HasValue)
{ {
value = _localValue; return (_localValue, BindingPriority.LocalValue);
ValuePriority = BindingPriority.LocalValue;
} }
return (default, BindingPriority.Unset);
}
private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs<T>? change)
{
var (value, priority) = CalculateValue(BindingPriority.Animation);
if (value.HasValue && _coerceValue != null) if (value.HasValue && _coerceValue != null)
{ {
value = _coerceValue(_owner, value.Value); value = _coerceValue(_owner, value.Value);
} }
if (value != Value) Priority = priority;
if (value != _value)
{
var old = _value;
_value = value;
_sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
Property,
old,
value,
Priority));
}
else if (change is object)
{ {
var old = Value; change.MarkNonEffectiveValue();
Value = value; change.SetOldValue(default);
_sink.ValueChanged(Property, ValuePriority, old, value); _sink.ValueChanged(change);
} }
} }
} }

7
src/Avalonia.Base/StyledPropertyBase.cs

@ -197,6 +197,13 @@ namespace Avalonia
return o.GetValue<TValue>(this); return o.GetValue<TValue>(this);
} }
/// <inheritdoc/>
internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority)
{
var value = o.GetBaseValue<TValue>(this, maxPriority);
return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue;
}
/// <inheritdoc/> /// <inheritdoc/>
internal override IDisposable RouteSetValue( internal override IDisposable RouteSetValue(
IAvaloniaObject o, IAvaloniaObject o,

139
src/Avalonia.Base/Utilities/MathUtilities.cs

@ -1,5 +1,4 @@
using System; using System;
using System.Runtime.InteropServices;
namespace Avalonia.Utilities namespace Avalonia.Utilities
{ {
@ -8,6 +7,11 @@ namespace Avalonia.Utilities
/// </summary> /// </summary>
public static class MathUtilities public static class MathUtilities
{ {
// smallest such that 1.0+DoubleEpsilon != 1.0
internal static readonly double DoubleEpsilon = 2.2204460492503131e-016;
private const float FloatEpsilon = 1.192092896e-07F;
/// <summary> /// <summary>
/// AreClose - Returns whether or not two doubles are "close". That is, whether or /// AreClose - Returns whether or not two doubles are "close". That is, whether or
/// not they are within epsilon of each other. /// not they are within epsilon of each other.
@ -18,11 +22,26 @@ namespace Avalonia.Utilities
{ {
//in case they are Infinities (then epsilon check does not work) //in case they are Infinities (then epsilon check does not work)
if (value1 == value2) return true; if (value1 == value2) return true;
double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * double.Epsilon; double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DoubleEpsilon;
double delta = value1 - value2; double delta = value1 - value2;
return (-eps < delta) && (eps > delta); return (-eps < delta) && (eps > delta);
} }
/// <summary>
/// AreClose - Returns whether or not two floats are "close". That is, whether or
/// not they are within epsilon of each other.
/// </summary>
/// <param name="value1"> The first float to compare. </param>
/// <param name="value2"> The second float to compare. </param>
public static bool AreClose(float value1, float value2)
{
//in case they are Infinities (then epsilon check does not work)
if (value1 == value2) return true;
float eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0f) * FloatEpsilon;
float delta = value1 - value2;
return (-eps < delta) && (eps > delta);
}
/// <summary> /// <summary>
/// LessThan - Returns whether or not the first double is less than the second double. /// LessThan - Returns whether or not the first double is less than the second double.
/// That is, whether or not the first is strictly less than *and* not within epsilon of /// That is, whether or not the first is strictly less than *and* not within epsilon of
@ -35,6 +54,18 @@ namespace Avalonia.Utilities
return (value1 < value2) && !AreClose(value1, value2); return (value1 < value2) && !AreClose(value1, value2);
} }
/// <summary>
/// LessThan - Returns whether or not the first float is less than the second float.
/// That is, whether or not the first is strictly less than *and* not within epsilon of
/// the other number.
/// </summary>
/// <param name="value1"> The first single float to compare. </param>
/// <param name="value2"> The second single float to compare. </param>
public static bool LessThan(float value1, float value2)
{
return (value1 < value2) && !AreClose(value1, value2);
}
/// <summary> /// <summary>
/// GreaterThan - Returns whether or not the first double is greater than the second double. /// GreaterThan - Returns whether or not the first double is greater than the second double.
/// That is, whether or not the first is strictly greater than *and* not within epsilon of /// That is, whether or not the first is strictly greater than *and* not within epsilon of
@ -47,6 +78,18 @@ namespace Avalonia.Utilities
return (value1 > value2) && !AreClose(value1, value2); return (value1 > value2) && !AreClose(value1, value2);
} }
/// <summary>
/// GreaterThan - Returns whether or not the first float is greater than the second float.
/// That is, whether or not the first is strictly greater than *and* not within epsilon of
/// the other number.
/// </summary>
/// <param name="value1"> The first float to compare. </param>
/// <param name="value2"> The second float to compare. </param>
public static bool GreaterThan(float value1, float value2)
{
return (value1 > value2) && !AreClose(value1, value2);
}
/// <summary> /// <summary>
/// LessThanOrClose - Returns whether or not the first double is less than or close to /// LessThanOrClose - Returns whether or not the first double is less than or close to
/// the second double. That is, whether or not the first is strictly less than or within /// the second double. That is, whether or not the first is strictly less than or within
@ -59,6 +102,18 @@ namespace Avalonia.Utilities
return (value1 < value2) || AreClose(value1, value2); return (value1 < value2) || AreClose(value1, value2);
} }
/// <summary>
/// LessThanOrClose - Returns whether or not the first float is less than or close to
/// the second float. That is, whether or not the first is strictly less than or within
/// epsilon of the other number.
/// </summary>
/// <param name="value1"> The first float to compare. </param>
/// <param name="value2"> The second float to compare. </param>
public static bool LessThanOrClose(float value1, float value2)
{
return (value1 < value2) || AreClose(value1, value2);
}
/// <summary> /// <summary>
/// GreaterThanOrClose - Returns whether or not the first double is greater than or close to /// GreaterThanOrClose - Returns whether or not the first double is greater than or close to
/// the second double. That is, whether or not the first is strictly greater than or within /// the second double. That is, whether or not the first is strictly greater than or within
@ -71,6 +126,18 @@ namespace Avalonia.Utilities
return (value1 > value2) || AreClose(value1, value2); return (value1 > value2) || AreClose(value1, value2);
} }
/// <summary>
/// GreaterThanOrClose - Returns whether or not the first float is greater than or close to
/// the second float. That is, whether or not the first is strictly greater than or within
/// epsilon of the other number.
/// </summary>
/// <param name="value1"> The first float to compare. </param>
/// <param name="value2"> The second float to compare. </param>
public static bool GreaterThanOrClose(float value1, float value2)
{
return (value1 > value2) || AreClose(value1, value2);
}
/// <summary> /// <summary>
/// IsOne - Returns whether or not the double is "close" to 1. Same as AreClose(double, 1), /// IsOne - Returns whether or not the double is "close" to 1. Same as AreClose(double, 1),
/// but this is faster. /// but this is faster.
@ -78,7 +145,17 @@ namespace Avalonia.Utilities
/// <param name="value"> The double to compare to 1. </param> /// <param name="value"> The double to compare to 1. </param>
public static bool IsOne(double value) public static bool IsOne(double value)
{ {
return Math.Abs(value - 1.0) < 10.0 * double.Epsilon; return Math.Abs(value - 1.0) < 10.0 * DoubleEpsilon;
}
/// <summary>
/// IsOne - Returns whether or not the float is "close" to 1. Same as AreClose(float, 1),
/// but this is faster.
/// </summary>
/// <param name="value"> The float to compare to 1. </param>
public static bool IsOne(float value)
{
return Math.Abs(value - 1.0f) < 10.0f * FloatEpsilon;
} }
/// <summary> /// <summary>
@ -88,7 +165,17 @@ namespace Avalonia.Utilities
/// <param name="value"> The double to compare to 0. </param> /// <param name="value"> The double to compare to 0. </param>
public static bool IsZero(double value) public static bool IsZero(double value)
{ {
return Math.Abs(value) < 10.0 * double.Epsilon; return Math.Abs(value) < 10.0 * DoubleEpsilon;
}
/// <summary>
/// IsZero - Returns whether or not the float is "close" to 0. Same as AreClose(float, 0),
/// but this is faster.
/// </summary>
/// <param name="value"> The float to compare to 0. </param>
public static bool IsZero(float value)
{
return Math.Abs(value) < 10.0f * FloatEpsilon;
} }
/// <summary> /// <summary>
@ -100,6 +187,11 @@ namespace Avalonia.Utilities
/// <returns>The clamped value.</returns> /// <returns>The clamped value.</returns>
public static double Clamp(double val, double min, double max) public static double Clamp(double val, double min, double max)
{ {
if (min > max)
{
ThrowCannotBeGreaterThanException(min, max);
}
if (val < min) if (val < min)
{ {
return min; return min;
@ -128,7 +220,7 @@ namespace Avalonia.Utilities
double newValue; double newValue;
// If DPI == 1, don't use DPI-aware rounding. // If DPI == 1, don't use DPI-aware rounding.
if (!MathUtilities.AreClose(dpiScale, 1.0)) if (!MathUtilities.IsOne(dpiScale))
{ {
newValue = Math.Round(value * dpiScale) / dpiScale; newValue = Math.Round(value * dpiScale) / dpiScale;
// If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value. // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value.
@ -158,7 +250,7 @@ namespace Avalonia.Utilities
{ {
if (min > max) if (min > max)
{ {
throw new ArgumentException($"{min} cannot be greater than {max}."); ThrowCannotBeGreaterThanException(min, max);
} }
if (val < min) if (val < min)
@ -174,5 +266,40 @@ namespace Avalonia.Utilities
return val; return val;
} }
} }
/// <summary>
/// Converts an angle in degrees to radians.
/// </summary>
/// <param name="angle">The angle in degrees.</param>
/// <returns>The angle in radians.</returns>
public static double Deg2Rad(double angle)
{
return angle * (Math.PI / 180d);
}
/// <summary>
/// Converts an angle in gradians to radians.
/// </summary>
/// <param name="angle">The angle in gradians.</param>
/// <returns>The angle in radians.</returns>
public static double Grad2Rad(double angle)
{
return angle * (Math.PI / 200d);
}
/// <summary>
/// Converts an angle in turns to radians.
/// </summary>
/// <param name="angle">The angle in turns.</param>
/// <returns>The angle in radians.</returns>
public static double Turn2Rad(double angle)
{
return angle * 2 * Math.PI;
}
private static void ThrowCannotBeGreaterThanException(double min, double max)
{
throw new ArgumentException($"{min} cannot be greater than {max}.");
}
} }
} }

43
src/Avalonia.Base/Utilities/TypeUtilities.cs

@ -115,45 +115,46 @@ namespace Avalonia.Utilities
return true; return true;
} }
var toUnderl = Nullable.GetUnderlyingType(to) ?? to;
var from = value.GetType(); var from = value.GetType();
if (to.IsAssignableFrom(from)) if (toUnderl.IsAssignableFrom(from))
{ {
result = value; result = value;
return true; return true;
} }
if (to == typeof(string)) if (toUnderl == typeof(string))
{ {
result = Convert.ToString(value); result = Convert.ToString(value, culture);
return true; 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; return true;
} }
} }
if (!from.IsEnum && to.IsEnum) if (!from.IsEnum && toUnderl.IsEnum)
{ {
result = null; 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; return true;
} }
} }
if (from.IsEnum && IsNumeric(to)) if (from.IsEnum && IsNumeric(toUnderl))
{ {
try try
{ {
result = Convert.ChangeType((int)value, to, culture); result = Convert.ChangeType((int)value, toUnderl, culture);
return true; return true;
} }
catch catch
@ -164,7 +165,7 @@ namespace Avalonia.Utilities
} }
var convertableFrom = Array.IndexOf(InbuiltTypes, from); var convertableFrom = Array.IndexOf(InbuiltTypes, from);
var convertableTo = Array.IndexOf(InbuiltTypes, to); var convertableTo = Array.IndexOf(InbuiltTypes, toUnderl);
if (convertableFrom != -1 && convertableTo != -1) if (convertableFrom != -1 && convertableTo != -1)
{ {
@ -172,7 +173,7 @@ namespace Avalonia.Utilities
{ {
try try
{ {
result = Convert.ChangeType(value, to, culture); result = Convert.ChangeType(value, toUnderl, culture);
return true; return true;
} }
catch 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; return true;
} }
var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit | OperatorType.Explicit); var cast = FindTypeConversionOperatorMethod(from, toUnderl, OperatorType.Implicit | OperatorType.Explicit);
if (cast != null) if (cast != null)
{ {

69
src/Avalonia.Base/ValueStore.cs

@ -37,7 +37,7 @@ namespace Avalonia
{ {
if (_values.TryGetValue(property, out var slot)) if (_values.TryGetValue(property, out var slot))
{ {
return slot.ValuePriority < BindingPriority.LocalValue; return slot.Priority < BindingPriority.LocalValue;
} }
return false; return false;
@ -47,21 +47,24 @@ namespace Avalonia
{ {
if (_values.TryGetValue(property, out var slot)) if (_values.TryGetValue(property, out var slot))
{ {
return slot.Value.HasValue; return slot.GetValue().HasValue;
} }
return false; return false;
} }
public bool TryGetValue<T>(StyledPropertyBase<T> property, out T value) public bool TryGetValue<T>(
StyledPropertyBase<T> property,
BindingPriority maxPriority,
out T value)
{ {
if (_values.TryGetValue(property, out var slot)) if (_values.TryGetValue(property, out var slot))
{ {
var v = (IValue<T>)slot; var v = ((IValue<T>)slot).GetValue(maxPriority);
if (v.Value.HasValue) if (v.HasValue)
{ {
value = v.Value.Value; value = v.Value;
return true; return true;
} }
} }
@ -90,17 +93,22 @@ namespace Avalonia
_values.AddValue(property, entry); _values.AddValue(property, entry);
result = entry.SetValue(value, priority); result = entry.SetValue(value, priority);
} }
else if (priority == BindingPriority.LocalValue)
{
_values.AddValue(property, new LocalValueEntry<T>(value));
_sink.ValueChanged(property, priority, default, value);
}
else else
{ {
var entry = new ConstantValueEntry<T>(property, value, priority, this); var change = new AvaloniaPropertyChangedEventArgs<T>(_owner, property, default, value, priority);
_values.AddValue(property, entry);
_sink.ValueChanged(property, priority, default, value); if (priority == BindingPriority.LocalValue)
result = entry; {
_values.AddValue(property, new LocalValueEntry<T>(value));
_sink.ValueChanged(change);
}
else
{
var entry = new ConstantValueEntry<T>(property, value, priority, this);
_values.AddValue(property, entry);
_sink.ValueChanged(change);
result = entry;
}
} }
return result; return result;
@ -149,13 +157,14 @@ namespace Avalonia
if (remove) 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); _values.Remove(property);
_sink.ValueChanged( _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
property, property,
BindingPriority.Unset,
old, old,
BindingValue<T>.Unset); default,
BindingPriority.Unset));
} }
} }
} }
@ -176,23 +185,20 @@ namespace Avalonia
{ {
if (_values.TryGetValue(property, out var slot)) if (_values.TryGetValue(property, out var slot))
{ {
var slotValue = slot.GetValue();
return new Diagnostics.AvaloniaPropertyValue( return new Diagnostics.AvaloniaPropertyValue(
property, property,
slot.Value.HasValue ? slot.Value.Value : AvaloniaProperty.UnsetValue, slotValue.HasValue ? slotValue.Value : AvaloniaProperty.UnsetValue,
slot.ValuePriority, slot.Priority,
null); null);
} }
return null; return null;
} }
void IValueSink.ValueChanged<T>( void IValueSink.ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
StyledPropertyBase<T> property,
BindingPriority priority,
Optional<T> oldValue,
BindingValue<T> newValue)
{ {
_sink.ValueChanged(property, priority, oldValue, newValue); _sink.ValueChanged(change);
} }
void IValueSink.Completed<T>( void IValueSink.Completed<T>(
@ -232,9 +238,14 @@ namespace Avalonia
{ {
if (priority == BindingPriority.LocalValue) if (priority == BindingPriority.LocalValue)
{ {
var old = l.Value; var old = l.GetValue(BindingPriority.LocalValue);
l.SetValue(value); l.SetValue(value);
_sink.ValueChanged(property, priority, old, value); _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
property,
old,
value,
priority));
} }
else else
{ {

11
src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs

@ -12,6 +12,8 @@ namespace Avalonia.Build.Tasks
{ {
public bool Execute() public bool Execute()
{ {
Enum.TryParse(ReportImportance, true, out MessageImportance outputImportance);
OutputPath = OutputPath ?? AssemblyFile; OutputPath = OutputPath ?? AssemblyFile;
var outputPdb = GetPdbPath(OutputPath); var outputPdb = GetPdbPath(OutputPath);
var input = AssemblyFile; 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, var res = XamlCompilerTaskExecutor.Compile(BuildEngine, input,
File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(), File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(),
ProjectDirectory, OutputPath, VerifyIl); ProjectDirectory, OutputPath, VerifyIl, outputImportance);
if (!res.Success) if (!res.Success)
return false; return false;
if (!res.WrittenFile) if (!res.WrittenFile)
@ -68,7 +73,9 @@ namespace Avalonia.Build.Tasks
public string OutputPath { get; set; } public string OutputPath { get; set; }
public bool VerifyIl { get; set; } public bool VerifyIl { get; set; }
public string ReportImportance { get; set; }
public IBuildEngine BuildEngine { get; set; } public IBuildEngine BuildEngine { get; set; }
public ITaskHost HostObject { get; set; } public ITaskHost HostObject { get; set; }
} }

9
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) 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")); "", "Avalonia"));
} }
public static void LogWarning(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message) 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, engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message,
"", "Avalonia")); "", "Avalonia"));
} }
public static void LogMessage(this IBuildEngine engine, string message, MessageImportance imp)
{
engine.LogMessageEvent(new BuildMessageEventArgs(message, "", "Avalonia", imp));
}
} }
} }

31
src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs

@ -22,6 +22,10 @@ namespace Avalonia.Build.Tasks
[Required] [Required]
public ITaskItem[] EmbeddedResources { get; set; } public ITaskItem[] EmbeddedResources { get; set; }
public string ReportImportance { get; set; }
private MessageImportance _reportImportance;
class Source class Source
{ {
public string Path { get; set; } public string Path { get; set; }
@ -29,15 +33,11 @@ namespace Avalonia.Build.Tasks
private byte[] _data; private byte[] _data;
private string _sourcePath; private string _sourcePath;
public Source(string file, string root) public Source(string relativePath, string root)
{ {
file = SPath.GetFullPath(file);
root = SPath.GetFullPath(root); root = SPath.GetFullPath(root);
var fileUri = new Uri(file, UriKind.Absolute); Path = "/" + relativePath.Replace('\\', '/');
var rootUri = new Uri(root, UriKind.Absolute); _sourcePath = SPath.Combine(root, relativePath);
rootUri = new Uri(rootUri.ToString().TrimEnd('/') + '/');
Path = '/' + rootUri.MakeRelativeUri(fileUri).ToString().TrimStart('/');
_sourcePath = file;
Size = (int)new FileInfo(_sourcePath).Length; Size = (int)new FileInfo(_sourcePath).Length;
} }
@ -65,7 +65,14 @@ namespace Avalonia.Build.Tasks
} }
} }
List<Source> BuildResourceSources() => Resources.Select(r => new Source(r.ItemSpec, Root)).ToList(); List<Source> 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<Source> sources) private void Pack(Stream output, List<Source> sources)
{ {
@ -136,10 +143,14 @@ namespace Avalonia.Build.Tasks
sources.Add(new Source("/!AvaloniaResourceXamlInfo", ms.ToArray())); sources.Add(new Source("/!AvaloniaResourceXamlInfo", ms.ToArray()));
return true; return true;
} }
public bool Execute() public bool Execute()
{ {
foreach(var r in EmbeddedResources.Where(r=>r.ItemSpec.EndsWith(".xaml")||r.ItemSpec.EndsWith(".paml"))) Enum.TryParse<MessageImportance>(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, BuildEngine.LogWarning(BuildEngineErrorCode.LegacyResmScheme, r.ItemSpec,
"XAML file is packed using legacy EmbeddedResource/resm scheme, relative URIs won't work"); "XAML file is packed using legacy EmbeddedResource/resm scheme, relative URIs won't work");
var resources = BuildResourceSources(); var resources = BuildResourceSources();

4
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, 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 typeSystem = new CecilTypeSystem(references.Concat(new[] {input}), input);
var asm = typeSystem.TargetAssemblyDefinition; var asm = typeSystem.TargetAssemblyDefinition;
@ -121,6 +121,8 @@ namespace Avalonia.Build.Tasks
{ {
try try
{ {
engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", logImportance);
// StreamReader is needed here to handle BOM // StreamReader is needed here to handle BOM
var xaml = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd(); var xaml = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd();
var parsed = XDocumentXamlIlParser.Parse(xaml); var parsed = XDocumentXamlIlParser.Parse(xaml);

43
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. // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved. // All other rights reserved.
@ -767,7 +766,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// ItemsProperty property changed handler. /// ItemsProperty property changed handler.
/// </summary> /// </summary>
/// <param name="e">AvaloniaPropertyChangedEventArgs.</param> /// <param name="e">The event arguments.</param>
private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e) private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{ {
if (!_areHandlersSuspended) if (!_areHandlersSuspended)
@ -2682,7 +2681,7 @@ namespace Avalonia.Controls
{ {
return; return;
} }
Debug.Assert(DoubleUtil.LessThanOrClose(_vScrollBar.Value, _vScrollBar.Maximum)); Debug.Assert(MathUtilities.LessThanOrClose(_vScrollBar.Value, _vScrollBar.Maximum));
_verticalScrollChangesIgnored++; _verticalScrollChangesIgnored++;
try try
@ -2699,7 +2698,7 @@ namespace Avalonia.Controls
} }
else if (scrollEventType == ScrollEventType.SmallDecrement) else if (scrollEventType == ScrollEventType.SmallDecrement)
{ {
if (DoubleUtil.GreaterThan(NegVerticalOffset, 0)) if (MathUtilities.GreaterThan(NegVerticalOffset, 0))
{ {
DisplayData.PendingVerticalScrollHeight -= NegVerticalOffset; DisplayData.PendingVerticalScrollHeight -= NegVerticalOffset;
} }
@ -2718,7 +2717,7 @@ namespace Avalonia.Controls
DisplayData.PendingVerticalScrollHeight = _vScrollBar.Value - _verticalOffset; DisplayData.PendingVerticalScrollHeight = _vScrollBar.Value - _verticalOffset;
} }
if (!DoubleUtil.IsZero(DisplayData.PendingVerticalScrollHeight)) if (!MathUtilities.IsZero(DisplayData.PendingVerticalScrollHeight))
{ {
// Invalidate so the scroll happens on idle // Invalidate so the scroll happens on idle
InvalidateRowsMeasure(invalidateIndividualElements: false); InvalidateRowsMeasure(invalidateIndividualElements: false);
@ -3347,22 +3346,22 @@ namespace Avalonia.Controls
bool needHorizScrollbarWithoutVertScrollbar = false; bool needHorizScrollbarWithoutVertScrollbar = false;
if (allowHorizScrollbar && if (allowHorizScrollbar &&
DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) && MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) &&
DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth) && MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth) &&
DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight)) MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight))
{ {
double oldDataHeight = cellsHeight; double oldDataHeight = cellsHeight;
cellsHeight -= horizScrollBarHeight; cellsHeight -= horizScrollBarHeight;
Debug.Assert(cellsHeight >= 0); Debug.Assert(cellsHeight >= 0);
needHorizScrollbarWithoutVertScrollbar = needHorizScrollbar = true; needHorizScrollbarWithoutVertScrollbar = needHorizScrollbar = true;
if (allowVertScrollbar && (DoubleUtil.LessThanOrClose(totalVisibleWidth - cellsWidth, vertScrollBarWidth) || if (allowVertScrollbar && (MathUtilities.LessThanOrClose(totalVisibleWidth - cellsWidth, vertScrollBarWidth) ||
DoubleUtil.LessThanOrClose(cellsWidth - totalVisibleFrozenWidth, vertScrollBarWidth))) MathUtilities.LessThanOrClose(cellsWidth - totalVisibleFrozenWidth, vertScrollBarWidth)))
{ {
// Would we still need a horizontal scrollbar without the vertical one? // Would we still need a horizontal scrollbar without the vertical one?
UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight); UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight);
if (DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount) if (DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount)
{ {
needHorizScrollbar = DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth - vertScrollBarWidth); needHorizScrollbar = MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth - vertScrollBarWidth);
} }
} }
@ -3375,8 +3374,8 @@ namespace Avalonia.Controls
UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight); UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight);
if (allowVertScrollbar && if (allowVertScrollbar &&
DoubleUtil.GreaterThan(cellsHeight, 0) && MathUtilities.GreaterThan(cellsHeight, 0) &&
DoubleUtil.LessThanOrClose(vertScrollBarWidth, cellsWidth) && MathUtilities.LessThanOrClose(vertScrollBarWidth, cellsWidth) &&
DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount) DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount)
{ {
cellsWidth -= vertScrollBarWidth; cellsWidth -= vertScrollBarWidth;
@ -3390,9 +3389,9 @@ namespace Avalonia.Controls
if (allowHorizScrollbar && if (allowHorizScrollbar &&
needVertScrollbar && !needHorizScrollbar && needVertScrollbar && !needHorizScrollbar &&
DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) && MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) &&
DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth) && MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth) &&
DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight)) MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight))
{ {
cellsWidth += vertScrollBarWidth; cellsWidth += vertScrollBarWidth;
cellsHeight -= horizScrollBarHeight; cellsHeight -= horizScrollBarHeight;
@ -3423,7 +3422,7 @@ namespace Avalonia.Controls
if (allowVertScrollbar) if (allowVertScrollbar)
{ {
if (cellsHeight > 0 && if (cellsHeight > 0 &&
DoubleUtil.LessThanOrClose(vertScrollBarWidth, cellsWidth) && MathUtilities.LessThanOrClose(vertScrollBarWidth, cellsWidth) &&
DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount) DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount)
{ {
cellsWidth -= vertScrollBarWidth; cellsWidth -= vertScrollBarWidth;
@ -3440,9 +3439,9 @@ namespace Avalonia.Controls
if (allowHorizScrollbar) if (allowHorizScrollbar)
{ {
if (cellsWidth > 0 && if (cellsWidth > 0 &&
DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight) && MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight) &&
DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) && MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) &&
DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth)) MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth))
{ {
cellsHeight -= horizScrollBarHeight; cellsHeight -= horizScrollBarHeight;
Debug.Assert(cellsHeight >= 0); Debug.Assert(cellsHeight >= 0);
@ -5388,7 +5387,7 @@ namespace Avalonia.Controls
private void SetVerticalOffset(double newVerticalOffset) private void SetVerticalOffset(double newVerticalOffset)
{ {
_verticalOffset = newVerticalOffset; _verticalOffset = newVerticalOffset;
if (_vScrollBar != null && !DoubleUtil.AreClose(newVerticalOffset, _vScrollBar.Value)) if (_vScrollBar != null && !MathUtilities.AreClose(newVerticalOffset, _vScrollBar.Value))
{ {
_vScrollBar.Value = _verticalOffset; _vScrollBar.Value = _verticalOffset;
} }

2
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@ -301,7 +301,7 @@ namespace Avalonia.Controls
private static bool CanResizeColumn(DataGridColumn column) private static bool CanResizeColumn(DataGridColumn column)
{ {
if (column.OwningGrid != null && column.OwningGrid.ColumnsInternal != null && column.OwningGrid.UsesStarSizing && if (column.OwningGrid != null && column.OwningGrid.ColumnsInternal != null && column.OwningGrid.UsesStarSizing &&
(column.OwningGrid.ColumnsInternal.LastVisibleColumn == column || !DoubleUtil.AreClose(column.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, column.OwningGrid.CellsWidth))) (column.OwningGrid.ColumnsInternal.LastVisibleColumn == column || !MathUtilities.AreClose(column.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, column.OwningGrid.CellsWidth)))
{ {
return false; return false;
} }

16
src/Avalonia.Controls.DataGrid/DataGridColumns.cs

@ -44,7 +44,7 @@ namespace Avalonia.Controls
/// <returns>The remaining amount of adjustment.</returns> /// <returns>The remaining amount of adjustment.</returns>
internal double AdjustColumnWidths(int displayIndex, double amount, bool userInitiated) internal double AdjustColumnWidths(int displayIndex, double amount, bool userInitiated)
{ {
if (!DoubleUtil.IsZero(amount)) if (!MathUtilities.IsZero(amount))
{ {
if (amount < 0) if (amount < 0)
{ {
@ -777,7 +777,7 @@ namespace Avalonia.Controls
private double AdjustStarColumnWidths(int displayIndex, double adjustment, bool userInitiated) private double AdjustStarColumnWidths(int displayIndex, double adjustment, bool userInitiated)
{ {
double remainingAdjustment = adjustment; double remainingAdjustment = adjustment;
if (DoubleUtil.IsZero(remainingAdjustment)) if (MathUtilities.IsZero(remainingAdjustment))
{ {
return remainingAdjustment; return remainingAdjustment;
} }
@ -843,7 +843,7 @@ namespace Avalonia.Controls
/// <returns>The remaining amount of adjustment.</returns> /// <returns>The remaining amount of adjustment.</returns>
private double AdjustStarColumnWidths(int displayIndex, double remainingAdjustment, bool userInitiated, Func<DataGridColumn, double> targetWidth) private double AdjustStarColumnWidths(int displayIndex, double remainingAdjustment, bool userInitiated, Func<DataGridColumn, double> targetWidth)
{ {
if (DoubleUtil.IsZero(remainingAdjustment)) if (MathUtilities.IsZero(remainingAdjustment))
{ {
return remainingAdjustment; return remainingAdjustment;
} }
@ -1244,7 +1244,7 @@ namespace Avalonia.Controls
Debug.Assert(amount < 0); Debug.Assert(amount < 0);
Debug.Assert(column.Width.UnitType != DataGridLengthUnitType.Star); Debug.Assert(column.Width.UnitType != DataGridLengthUnitType.Star);
if (DoubleUtil.GreaterThanOrClose(targetWidth, column.Width.DisplayValue)) if (MathUtilities.GreaterThanOrClose(targetWidth, column.Width.DisplayValue))
{ {
return amount; return amount;
} }
@ -1271,7 +1271,7 @@ namespace Avalonia.Controls
/// <returns>The remaining amount of adjustment.</returns> /// <returns>The remaining amount of adjustment.</returns>
private double DecreaseNonStarColumnWidths(int displayIndex, Func<DataGridColumn, double> targetWidth, double amount, bool reverse, bool affectNewColumns) private double DecreaseNonStarColumnWidths(int displayIndex, Func<DataGridColumn, double> targetWidth, double amount, bool reverse, bool affectNewColumns)
{ {
if (DoubleUtil.GreaterThanOrClose(amount, 0)) if (MathUtilities.GreaterThanOrClose(amount, 0))
{ {
return amount; return amount;
} }
@ -1285,7 +1285,7 @@ namespace Avalonia.Controls
(affectNewColumns || column.IsInitialDesiredWidthDetermined))) (affectNewColumns || column.IsInitialDesiredWidthDetermined)))
{ {
amount = DecreaseNonStarColumnWidth(column, Math.Max(column.ActualMinWidth, targetWidth(column)), amount); amount = DecreaseNonStarColumnWidth(column, Math.Max(column.ActualMinWidth, targetWidth(column)), amount);
if (DoubleUtil.IsZero(amount)) if (MathUtilities.IsZero(amount))
{ {
break; break;
} }
@ -1392,7 +1392,7 @@ namespace Avalonia.Controls
/// <returns>The remaining amount of adjustment.</returns> /// <returns>The remaining amount of adjustment.</returns>
private double IncreaseNonStarColumnWidths(int displayIndex, Func<DataGridColumn, double> targetWidth, double amount, bool reverse, bool affectNewColumns) private double IncreaseNonStarColumnWidths(int displayIndex, Func<DataGridColumn, double> targetWidth, double amount, bool reverse, bool affectNewColumns)
{ {
if (DoubleUtil.LessThanOrClose(amount, 0)) if (MathUtilities.LessThanOrClose(amount, 0))
{ {
return amount; return amount;
} }
@ -1406,7 +1406,7 @@ namespace Avalonia.Controls
(affectNewColumns || column.IsInitialDesiredWidthDetermined))) (affectNewColumns || column.IsInitialDesiredWidthDetermined)))
{ {
amount = IncreaseNonStarColumnWidth(column, Math.Min(column.ActualMaxWidth, targetWidth(column)), amount); amount = IncreaseNonStarColumnWidth(column, Math.Min(column.ActualMaxWidth, targetWidth(column)), amount);
if (DoubleUtil.IsZero(amount)) if (MathUtilities.IsZero(amount))
{ {
break; break;
} }

2
src/Avalonia.Controls.DataGrid/DataGridLength.cs

@ -529,7 +529,7 @@ namespace Avalonia.Controls
// in this case drop value part and print only "Star" // in this case drop value part and print only "Star"
case DataGridLengthUnitType.Star: case DataGridLengthUnitType.Star:
return ( return (
DoubleUtil.AreClose(1.0, dataGridLength.Value.Value) MathUtilities.AreClose(1.0, dataGridLength.Value.Value)
? _starSuffix ? _starSuffix
: Convert.ToString(dataGridLength.Value.Value, culture ?? CultureInfo.CurrentCulture) + DataGridLengthConverter._starSuffix); : Convert.ToString(dataGridLength.Value.Value, culture ?? CultureInfo.CurrentCulture) + DataGridLengthConverter._starSuffix);

2
src/Avalonia.Controls.DataGrid/DataGridRow.cs

@ -879,7 +879,7 @@ namespace Avalonia.Controls
&& (double.IsNaN(_detailsContent.Height)) && (double.IsNaN(_detailsContent.Height))
&& (AreDetailsVisible) && (AreDetailsVisible)
&& (!double.IsNaN(_detailsDesiredHeight)) && (!double.IsNaN(_detailsDesiredHeight))
&& !DoubleUtil.AreClose(_detailsContent.Bounds.Inflate(_detailsContent.Margin).Height, _detailsDesiredHeight) && !MathUtilities.AreClose(_detailsContent.Bounds.Inflate(_detailsContent.Margin).Height, _detailsDesiredHeight)
&& Slot != -1) && Slot != -1)
{ {
_detailsDesiredHeight = _detailsContent.Bounds.Inflate(_detailsContent.Margin).Height; _detailsDesiredHeight = _detailsContent.Bounds.Inflate(_detailsContent.Margin).Height;

60
src/Avalonia.Controls.DataGrid/DataGridRows.cs

@ -329,7 +329,7 @@ namespace Avalonia.Controls
internal void OnRowsMeasure() internal void OnRowsMeasure()
{ {
if (!DoubleUtil.IsZero(DisplayData.PendingVerticalScrollHeight)) if (!MathUtilities.IsZero(DisplayData.PendingVerticalScrollHeight))
{ {
ScrollSlotsByHeight(DisplayData.PendingVerticalScrollHeight); ScrollSlotsByHeight(DisplayData.PendingVerticalScrollHeight);
DisplayData.PendingVerticalScrollHeight = 0; DisplayData.PendingVerticalScrollHeight = 0;
@ -432,7 +432,7 @@ namespace Avalonia.Controls
} }
else if (DisplayData.FirstScrollingSlot == slot && slot != -1) else if (DisplayData.FirstScrollingSlot == slot && slot != -1)
{ {
if (!DoubleUtil.IsZero(NegVerticalOffset)) if (!MathUtilities.IsZero(NegVerticalOffset))
{ {
// First displayed row is partially scrolled of. Let's scroll it so that NegVerticalOffset becomes 0. // First displayed row is partially scrolled of. Let's scroll it so that NegVerticalOffset becomes 0.
DisplayData.PendingVerticalScrollHeight = -NegVerticalOffset; DisplayData.PendingVerticalScrollHeight = -NegVerticalOffset;
@ -447,7 +447,7 @@ namespace Avalonia.Controls
{ {
// Scroll up to the new row so it becomes the first displayed row // Scroll up to the new row so it becomes the first displayed row
firstFullSlot = DisplayData.FirstScrollingSlot - 1; firstFullSlot = DisplayData.FirstScrollingSlot - 1;
if (DoubleUtil.GreaterThan(NegVerticalOffset, 0)) if (MathUtilities.GreaterThan(NegVerticalOffset, 0))
{ {
deltaY = -NegVerticalOffset; deltaY = -NegVerticalOffset;
} }
@ -470,7 +470,7 @@ namespace Avalonia.Controls
// Figure out how much of the last row is cut off // Figure out how much of the last row is cut off
double rowHeight = GetExactSlotElementHeight(DisplayData.LastScrollingSlot); double rowHeight = GetExactSlotElementHeight(DisplayData.LastScrollingSlot);
double availableHeight = AvailableSlotElementRoom + rowHeight; double availableHeight = AvailableSlotElementRoom + rowHeight;
if (DoubleUtil.AreClose(rowHeight, availableHeight)) if (MathUtilities.AreClose(rowHeight, availableHeight))
{ {
if (DisplayData.LastScrollingSlot == slot) if (DisplayData.LastScrollingSlot == slot)
{ {
@ -499,7 +499,7 @@ namespace Avalonia.Controls
{ {
ResetDisplayedRows(); ResetDisplayedRows();
} }
if (DoubleUtil.GreaterThanOrClose(GetExactSlotElementHeight(slot), CellsHeight)) if (MathUtilities.GreaterThanOrClose(GetExactSlotElementHeight(slot), CellsHeight))
{ {
// The entire row won't fit in the DataGrid so we start showing it from the top // The entire row won't fit in the DataGrid so we start showing it from the top
NegVerticalOffset = 0; NegVerticalOffset = 0;
@ -519,7 +519,7 @@ namespace Avalonia.Controls
} }
// //
Debug.Assert(DoubleUtil.LessThanOrClose(NegVerticalOffset, _verticalOffset)); Debug.Assert(MathUtilities.LessThanOrClose(NegVerticalOffset, _verticalOffset));
SetVerticalOffset(_verticalOffset); SetVerticalOffset(_verticalOffset);
@ -1660,7 +1660,7 @@ namespace Avalonia.Controls
private void ScrollSlotsByHeight(double height) private void ScrollSlotsByHeight(double height)
{ {
Debug.Assert(DisplayData.FirstScrollingSlot >= 0); Debug.Assert(DisplayData.FirstScrollingSlot >= 0);
Debug.Assert(!DoubleUtil.IsZero(height)); Debug.Assert(!MathUtilities.IsZero(height));
_scrollingByHeight = true; _scrollingByHeight = true;
try try
@ -1672,7 +1672,7 @@ namespace Avalonia.Controls
{ {
// Scrolling Down // Scrolling Down
int lastVisibleSlot = GetPreviousVisibleSlot(SlotCount); int lastVisibleSlot = GetPreviousVisibleSlot(SlotCount);
if (_vScrollBar != null && DoubleUtil.AreClose(_vScrollBar.Maximum, newVerticalOffset)) if (_vScrollBar != null && MathUtilities.AreClose(_vScrollBar.Maximum, newVerticalOffset))
{ {
// We've scrolled to the bottom of the ScrollBar, automatically place the user at the very bottom // We've scrolled to the bottom of the ScrollBar, automatically place the user at the very bottom
// of the DataGrid. If this produces very odd behavior, evaluate the coping strategy used by // of the DataGrid. If this produces very odd behavior, evaluate the coping strategy used by
@ -1684,7 +1684,7 @@ namespace Avalonia.Controls
else else
{ {
deltaY = GetSlotElementHeight(newFirstScrollingSlot) - NegVerticalOffset; deltaY = GetSlotElementHeight(newFirstScrollingSlot) - NegVerticalOffset;
if (DoubleUtil.LessThan(height, deltaY)) if (MathUtilities.LessThan(height, deltaY))
{ {
// We've merely covered up more of the same row we're on // We've merely covered up more of the same row we're on
NegVerticalOffset += height; NegVerticalOffset += height;
@ -1707,7 +1707,7 @@ namespace Avalonia.Controls
} }
else else
{ {
while (DoubleUtil.LessThanOrClose(deltaY, height)) while (MathUtilities.LessThanOrClose(deltaY, height))
{ {
if (newFirstScrollingSlot < lastVisibleSlot) if (newFirstScrollingSlot < lastVisibleSlot)
{ {
@ -1727,7 +1727,7 @@ namespace Avalonia.Controls
double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot); double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot);
double remainingHeight = height - deltaY; double remainingHeight = height - deltaY;
if (DoubleUtil.LessThanOrClose(rowHeight, remainingHeight)) if (MathUtilities.LessThanOrClose(rowHeight, remainingHeight))
{ {
deltaY += rowHeight; deltaY += rowHeight;
} }
@ -1744,7 +1744,7 @@ namespace Avalonia.Controls
else else
{ {
// Scrolling Up // Scrolling Up
if (DoubleUtil.GreaterThanOrClose(height + NegVerticalOffset, 0)) if (MathUtilities.GreaterThanOrClose(height + NegVerticalOffset, 0))
{ {
// We've merely exposing more of the row we're on // We've merely exposing more of the row we're on
NegVerticalOffset += height; NegVerticalOffset += height;
@ -1778,7 +1778,7 @@ namespace Avalonia.Controls
else else
{ {
int lastScrollingSlot = DisplayData.LastScrollingSlot; int lastScrollingSlot = DisplayData.LastScrollingSlot;
while (DoubleUtil.GreaterThan(deltaY, height)) while (MathUtilities.GreaterThan(deltaY, height))
{ {
if (newFirstScrollingSlot > 0) if (newFirstScrollingSlot > 0)
{ {
@ -1797,7 +1797,7 @@ namespace Avalonia.Controls
} }
double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot); double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot);
double remainingHeight = height - deltaY; double remainingHeight = height - deltaY;
if (DoubleUtil.LessThanOrClose(rowHeight + remainingHeight, 0)) if (MathUtilities.LessThanOrClose(rowHeight + remainingHeight, 0))
{ {
deltaY -= rowHeight; deltaY -= rowHeight;
} }
@ -1809,7 +1809,7 @@ namespace Avalonia.Controls
} }
} }
} }
if (DoubleUtil.GreaterThanOrClose(0, newVerticalOffset) && newFirstScrollingSlot != 0) if (MathUtilities.GreaterThanOrClose(0, newVerticalOffset) && newFirstScrollingSlot != 0)
{ {
// We've scrolled to the top of the ScrollBar, automatically place the user at the very top // We've scrolled to the top of the ScrollBar, automatically place the user at the very top
// of the DataGrid. If this produces very odd behavior, evaluate the RowHeight estimate. // of the DataGrid. If this produces very odd behavior, evaluate the RowHeight estimate.
@ -1822,7 +1822,7 @@ namespace Avalonia.Controls
} }
double firstRowHeight = GetExactSlotElementHeight(newFirstScrollingSlot); double firstRowHeight = GetExactSlotElementHeight(newFirstScrollingSlot);
if (DoubleUtil.LessThan(firstRowHeight, NegVerticalOffset)) if (MathUtilities.LessThan(firstRowHeight, NegVerticalOffset))
{ {
// We've scrolled off more of the first row than what's possible. This can happen // We've scrolled off more of the first row than what's possible. This can happen
// if the first row got shorter (Ex: Collpasing RowDetails) or if the user has a recycling // if the first row got shorter (Ex: Collpasing RowDetails) or if the user has a recycling
@ -1838,11 +1838,11 @@ namespace Avalonia.Controls
UpdateDisplayedRows(newFirstScrollingSlot, CellsHeight); UpdateDisplayedRows(newFirstScrollingSlot, CellsHeight);
double firstElementHeight = GetExactSlotElementHeight(DisplayData.FirstScrollingSlot); double firstElementHeight = GetExactSlotElementHeight(DisplayData.FirstScrollingSlot);
if (DoubleUtil.GreaterThan(NegVerticalOffset, firstElementHeight)) if (MathUtilities.GreaterThan(NegVerticalOffset, firstElementHeight))
{ {
int firstElementSlot = DisplayData.FirstScrollingSlot; int firstElementSlot = DisplayData.FirstScrollingSlot;
// We filled in some rows at the top and now we have a NegVerticalOffset that's greater than the first element // We filled in some rows at the top and now we have a NegVerticalOffset that's greater than the first element
while (newFirstScrollingSlot > 0 && DoubleUtil.GreaterThan(NegVerticalOffset, firstElementHeight)) while (newFirstScrollingSlot > 0 && MathUtilities.GreaterThan(NegVerticalOffset, firstElementHeight))
{ {
int previousSlot = GetPreviousVisibleSlot(firstElementSlot); int previousSlot = GetPreviousVisibleSlot(firstElementSlot);
if (previousSlot == -1) if (previousSlot == -1)
@ -1872,7 +1872,7 @@ namespace Avalonia.Controls
{ {
_verticalOffset = NegVerticalOffset; _verticalOffset = NegVerticalOffset;
} }
else if (DoubleUtil.GreaterThan(NegVerticalOffset, newVerticalOffset)) else if (MathUtilities.GreaterThan(NegVerticalOffset, newVerticalOffset))
{ {
// The scrolled-in row was larger than anticipated. Adjust the DataGrid so the ScrollBar thumb // The scrolled-in row was larger than anticipated. Adjust the DataGrid so the ScrollBar thumb
// can stay in the same place // can stay in the same place
@ -1890,8 +1890,8 @@ namespace Avalonia.Controls
DisplayData.FullyRecycleElements(); DisplayData.FullyRecycleElements();
Debug.Assert(DoubleUtil.GreaterThanOrClose(NegVerticalOffset, 0)); Debug.Assert(MathUtilities.GreaterThanOrClose(NegVerticalOffset, 0));
Debug.Assert(DoubleUtil.GreaterThanOrClose(_verticalOffset, NegVerticalOffset)); Debug.Assert(MathUtilities.GreaterThanOrClose(_verticalOffset, NegVerticalOffset));
} }
finally finally
{ {
@ -2032,7 +2032,7 @@ namespace Avalonia.Controls
double deltaY = -NegVerticalOffset; double deltaY = -NegVerticalOffset;
int visibleScrollingRows = 0; int visibleScrollingRows = 0;
if (DoubleUtil.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0) if (MathUtilities.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0)
{ {
return; return;
} }
@ -2044,7 +2044,7 @@ namespace Avalonia.Controls
} }
int slot = firstDisplayedScrollingSlot; int slot = firstDisplayedScrollingSlot;
while (slot < SlotCount && !DoubleUtil.GreaterThanOrClose(deltaY, displayHeight)) while (slot < SlotCount && !MathUtilities.GreaterThanOrClose(deltaY, displayHeight))
{ {
deltaY += GetExactSlotElementHeight(slot); deltaY += GetExactSlotElementHeight(slot);
visibleScrollingRows++; visibleScrollingRows++;
@ -2052,7 +2052,7 @@ namespace Avalonia.Controls
slot = GetNextVisibleSlot(slot); slot = GetNextVisibleSlot(slot);
} }
while (DoubleUtil.LessThan(deltaY, displayHeight) && slot >= 0) while (MathUtilities.LessThan(deltaY, displayHeight) && slot >= 0)
{ {
slot = GetPreviousVisibleSlot(firstDisplayedScrollingSlot); slot = GetPreviousVisibleSlot(firstDisplayedScrollingSlot);
if (slot >= 0) if (slot >= 0)
@ -2063,14 +2063,14 @@ namespace Avalonia.Controls
} }
} }
// If we're up to the first row, and we still have room left, uncover as much of the first row as we can // If we're up to the first row, and we still have room left, uncover as much of the first row as we can
if (firstDisplayedScrollingSlot == 0 && DoubleUtil.LessThan(deltaY, displayHeight)) if (firstDisplayedScrollingSlot == 0 && MathUtilities.LessThan(deltaY, displayHeight))
{ {
double newNegVerticalOffset = Math.Max(0, NegVerticalOffset - displayHeight + deltaY); double newNegVerticalOffset = Math.Max(0, NegVerticalOffset - displayHeight + deltaY);
deltaY += NegVerticalOffset - newNegVerticalOffset; deltaY += NegVerticalOffset - newNegVerticalOffset;
NegVerticalOffset = newNegVerticalOffset; NegVerticalOffset = newNegVerticalOffset;
} }
if (DoubleUtil.GreaterThan(deltaY, displayHeight) || (DoubleUtil.AreClose(deltaY, displayHeight) && DoubleUtil.GreaterThan(NegVerticalOffset, 0))) if (MathUtilities.GreaterThan(deltaY, displayHeight) || (MathUtilities.AreClose(deltaY, displayHeight) && MathUtilities.GreaterThan(NegVerticalOffset, 0)))
{ {
DisplayData.NumTotallyDisplayedScrollingElements = visibleScrollingRows - 1; DisplayData.NumTotallyDisplayedScrollingElements = visibleScrollingRows - 1;
} }
@ -2108,7 +2108,7 @@ namespace Avalonia.Controls
double deltaY = 0; double deltaY = 0;
int visibleScrollingRows = 0; int visibleScrollingRows = 0;
if (DoubleUtil.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0) if (MathUtilities.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0)
{ {
ResetDisplayedRows(); ResetDisplayedRows();
return; return;
@ -2120,7 +2120,7 @@ namespace Avalonia.Controls
} }
int slot = lastDisplayedScrollingRow; int slot = lastDisplayedScrollingRow;
while (DoubleUtil.LessThan(deltaY, displayHeight) && slot >= 0) while (MathUtilities.LessThan(deltaY, displayHeight) && slot >= 0)
{ {
deltaY += GetExactSlotElementHeight(slot); deltaY += GetExactSlotElementHeight(slot);
visibleScrollingRows++; visibleScrollingRows++;
@ -2542,7 +2542,7 @@ namespace Avalonia.Controls
double heightChange = UpdateRowGroupVisibility(rowGroupInfo, isVisible, isDisplayed: false); double heightChange = UpdateRowGroupVisibility(rowGroupInfo, isVisible, isDisplayed: false);
// Use epsilon instead of 0 here so that in the off chance that our estimates put the vertical offset negative // Use epsilon instead of 0 here so that in the off chance that our estimates put the vertical offset negative
// the user can still scroll to the top since the offset is non-zero // the user can still scroll to the top since the offset is non-zero
SetVerticalOffset(Math.Max(DoubleUtil.DBL_EPSILON, _verticalOffset + heightChange)); SetVerticalOffset(Math.Max(MathUtilities.DoubleEpsilon, _verticalOffset + heightChange));
} }
else else
{ {
@ -3024,4 +3024,4 @@ namespace Avalonia.Controls
} }
#endif #endif
} }
} }

6
src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs

@ -310,9 +310,9 @@ namespace Avalonia.Controls.Primitives
double leftEdge = column.IsFrozen ? frozenLeftEdge : scrollingLeftEdge; double leftEdge = column.IsFrozen ? frozenLeftEdge : scrollingLeftEdge;
double rightEdge = leftEdge + column.ActualWidth; double rightEdge = leftEdge + column.ActualWidth;
return return
DoubleUtil.GreaterThan(rightEdge, 0) && MathUtilities.GreaterThan(rightEdge, 0) &&
DoubleUtil.LessThanOrClose(leftEdge, OwningGrid.CellsWidth) && MathUtilities.LessThanOrClose(leftEdge, OwningGrid.CellsWidth) &&
DoubleUtil.GreaterThan(rightEdge, frozenLeftEdge); // scrolling column covered up by frozen column(s) MathUtilities.GreaterThan(rightEdge, frozenLeftEdge); // scrolling column covered up by frozen column(s)
} }
} }
} }

136
src/Avalonia.Controls.DataGrid/Utils/DoubleUtil.cs

@ -1,136 +0,0 @@
// (c) Copyright Microsoft Corporation.
// 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.
using System;
namespace Avalonia.Controls.Utils
{
internal static class DoubleUtil
{
internal const double DBL_EPSILON = 1e-6;
/// <summary>
/// AreClose - Returns whether or not two doubles are "close". That is, whether or
/// not they are within epsilon of each other. Note that this epsilon is proportional
/// to the numbers themselves to that AreClose survives scalar multiplication.
/// There are plenty of ways for this to return false even for numbers which
/// are theoretically identical, so no code calling this should fail to work if this
/// returns false. This is important enough to repeat:
/// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
/// used for optimizations *only*.
/// </summary>
/// <returns>
/// bool - the result of the AreClose comparison.
/// </returns>
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool AreClose(double value1, double value2)
{
//in case they are Infinities (then epsilon check does not work)
if (value1 == value2) return true;
// This computes (|value1-value2| / (|value1| + |value2| + 10.0)) < DBL_EPSILON
double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DBL_EPSILON;
double delta = value1 - value2;
return (-eps < delta) && (eps > delta);
}
/// <summary>
/// GreaterThan - Returns whether or not the first double is greater than the second double.
/// That is, whether or not the first is strictly greater than *and* not within epsilon of
/// the other number. Note that this epsilon is proportional to the numbers themselves
/// to that AreClose survives scalar multiplication. Note,
/// There are plenty of ways for this to return false even for numbers which
/// are theoretically identical, so no code calling this should fail to work if this
/// returns false. This is important enough to repeat:
/// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
/// used for optimizations *only*.
/// </summary>
/// <returns>
/// bool - the result of the GreaterThan comparison.
/// </returns>
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool GreaterThan(double value1, double value2)
{
return (value1 > value2) && !AreClose(value1, value2);
}
/// <summary>
/// GreaterThanOrClose - Returns whether or not the first double is greater than or close to
/// the second double. That is, whether or not the first is strictly greater than or within
/// epsilon of the other number. Note that this epsilon is proportional to the numbers
/// themselves to that AreClose survives scalar multiplication. Note,
/// There are plenty of ways for this to return false even for numbers which
/// are theoretically identical, so no code calling this should fail to work if this
/// returns false. This is important enough to repeat:
/// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
/// used for optimizations *only*.
/// </summary>
/// <returns>
/// bool - the result of the GreaterThanOrClose comparison.
/// </returns>
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool GreaterThanOrClose(double value1, double value2)
{
return (value1 > value2) || AreClose(value1, value2);
}
/// <summary>
/// IsZero - Returns whether or not the double is "close" to 0. Same as AreClose(double, 0),
/// but this is faster.
/// </summary>
/// <returns>
/// bool - the result of the IsZero comparison.
/// </returns>
/// <param name="value"> The double to compare to 0. </param>
public static bool IsZero(double value)
{
return Math.Abs(value) < 10.0 * DBL_EPSILON;
}
/// <summary>
/// LessThan - Returns whether or not the first double is less than the second double.
/// That is, whether or not the first is strictly less than *and* not within epsilon of
/// the other number. Note that this epsilon is proportional to the numbers themselves
/// to that AreClose survives scalar multiplication. Note,
/// There are plenty of ways for this to return false even for numbers which
/// are theoretically identical, so no code calling this should fail to work if this
/// returns false. This is important enough to repeat:
/// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
/// used for optimizations *only*.
/// </summary>
/// <returns>
/// bool - the result of the LessThan comparison.
/// </returns>
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool LessThan(double value1, double value2)
{
return (value1 < value2) && !AreClose(value1, value2);
}
/// <summary>
/// LessThanOrClose - Returns whether or not the first double is less than or close to
/// the second double. That is, whether or not the first is strictly less than or within
/// epsilon of the other number. Note that this epsilon is proportional to the numbers
/// themselves to that AreClose survives scalar multiplication. Note,
/// There are plenty of ways for this to return false even for numbers which
/// are theoretically identical, so no code calling this should fail to work if this
/// returns false. This is important enough to repeat:
/// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
/// used for optimizations *only*.
/// </summary>
/// <returns>
/// bool - the result of the LessThanOrClose comparison.
/// </returns>
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool LessThanOrClose(double value1, double value2)
{
return (value1 < value2) || AreClose(value1, value2);
}
}
}

68
src/Avalonia.Controls/Application.cs

@ -30,7 +30,7 @@ namespace Avalonia
/// method. /// method.
/// - Tracks the lifetime of the application. /// - Tracks the lifetime of the application.
/// </remarks> /// </remarks>
public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceNode public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost
{ {
/// <summary> /// <summary>
/// The application-global data templates. /// The application-global data templates.
@ -129,26 +129,13 @@ namespace Avalonia
/// </summary> /// </summary>
public IResourceDictionary Resources public IResourceDictionary Resources
{ {
get => _resources ?? (Resources = new ResourceDictionary()); get => _resources ??= new ResourceDictionary(this);
set set
{ {
Contract.Requires<ArgumentNullException>(value != null); value = value ?? throw new ArgumentNullException(nameof(value));
_resources?.RemoveOwner(this);
var hadResources = false;
if (_resources != null)
{
hadResources = _resources.Count > 0;
_resources.ResourcesChanged -= ThisResourcesChanged;
}
_resources = value; _resources = value;
_resources.ResourcesChanged += ThisResourcesChanged; _resources.AddOwner(this);
if (hadResources || _resources.Count > 0)
{
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
}
} }
} }
@ -161,23 +148,15 @@ namespace Avalonia
/// <remarks> /// <remarks>
/// Global styles apply to all windows in the application. /// Global styles apply to all windows in the application.
/// </remarks> /// </remarks>
public Styles Styles public Styles Styles => _styles ??= new Styles(this);
{
get
{
if (_styles == null)
{
_styles = new Styles(this);
_styles.ResourcesChanged += ThisResourcesChanged;
}
return _styles;
}
}
/// <inheritdoc/> /// <inheritdoc/>
bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
/// <inheritdoc/>
bool IResourceNode.HasResources => (_resources?.HasResources ?? false) ||
(((IResourceNode?)_styles)?.HasResources ?? false);
/// <summary> /// <summary>
/// Gets the styling parent of the application, which is null. /// Gets the styling parent of the application, which is null.
/// </summary> /// </summary>
@ -185,13 +164,7 @@ namespace Avalonia
/// <inheritdoc/> /// <inheritdoc/>
bool IStyleHost.IsStylesInitialized => _styles != null; bool IStyleHost.IsStylesInitialized => _styles != null;
/// <inheritdoc/>
bool IResourceProvider.HasResources => _resources?.Count > 0;
/// <inheritdoc/>
IResourceNode IResourceNode.ResourceParent => null;
/// <summary> /// <summary>
/// Application lifetime, use it for things like setting the main window and exiting the app from code /// Application lifetime, use it for things like setting the main window and exiting the app from code
/// Currently supported lifetimes are: /// Currently supported lifetimes are:
@ -219,13 +192,18 @@ namespace Avalonia
public virtual void Initialize() { } public virtual void Initialize() { }
/// <inheritdoc/> /// <inheritdoc/>
bool IResourceProvider.TryGetResource(object key, out object value) bool IResourceNode.TryGetResource(object key, out object value)
{ {
value = null; value = null;
return (_resources?.TryGetResource(key, out value) ?? false) || return (_resources?.TryGetResource(key, out value) ?? false) ||
Styles.TryGetResource(key, out value); Styles.TryGetResource(key, out value);
} }
void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e)
{
ResourcesChanged?.Invoke(this, e);
}
void IStyleHost.StylesAdded(IReadOnlyList<IStyle> styles) void IStyleHost.StylesAdded(IReadOnlyList<IStyle> styles)
{ {
_stylesAdded?.Invoke(styles); _stylesAdded?.Invoke(styles);
@ -254,8 +232,12 @@ namespace Avalonia
.Bind<IKeyboardNavigationHandler>().ToTransient<KeyboardNavigationHandler>() .Bind<IKeyboardNavigationHandler>().ToTransient<KeyboardNavigationHandler>()
.Bind<IStyler>().ToConstant(_styler) .Bind<IStyler>().ToConstant(_styler)
.Bind<IScheduler>().ToConstant(AvaloniaScheduler.Instance) .Bind<IScheduler>().ToConstant(AvaloniaScheduler.Instance)
.Bind<IDragDropDevice>().ToConstant(DragDropDevice.Instance) .Bind<IDragDropDevice>().ToConstant(DragDropDevice.Instance);
.Bind<IPlatformDragSource>().ToTransient<InProcessDragSource>();
// TODO: Fix this, for now we keep this behavior since someone might be relying on it in 0.9.x
if (AvaloniaLocator.Current.GetService<IPlatformDragSource>() == null)
AvaloniaLocator.CurrentMutable
.Bind<IPlatformDragSource>().ToTransient<InProcessDragSource>();
var clock = new RenderLoopClock(); var clock = new RenderLoopClock();
AvaloniaLocator.CurrentMutable AvaloniaLocator.CurrentMutable
@ -278,9 +260,7 @@ namespace Avalonia
try try
{ {
_notifyingResourcesChanged = true; _notifyingResourcesChanged = true;
(_resources as ISetResourceParent)?.ParentResourcesChanged(e); ResourcesChanged?.Invoke(this, ResourcesChangedEventArgs.Empty);
(_styles as ISetResourceParent)?.ParentResourcesChanged(e);
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
} }
finally finally
{ {

23
src/Avalonia.Controls/AutoCompleteBox.cs

@ -10,6 +10,7 @@ using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reactive.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Collections; using Avalonia.Collections;
@ -467,10 +468,11 @@ namespace Avalonia.Controls
/// <see cref="P:Avalonia.Controls.AutoCompleteBox.Text" /> /// <see cref="P:Avalonia.Controls.AutoCompleteBox.Text" />
/// dependency property.</value> /// dependency property.</value>
public static readonly DirectProperty<AutoCompleteBox, string> TextProperty = public static readonly DirectProperty<AutoCompleteBox, string> TextProperty =
AvaloniaProperty.RegisterDirect<AutoCompleteBox, string>( TextBlock.TextProperty.AddOwnerWithDataValidation<AutoCompleteBox>(
nameof(Text),
o => o.Text, o => o.Text,
(o, v) => o.Text = v); (o, v) => o.Text = v,
defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true);
/// <summary> /// <summary>
/// Identifies the /// Identifies the
@ -1100,6 +1102,7 @@ namespace Avalonia.Controls
{ {
_textBoxSubscriptions = _textBoxSubscriptions =
_textBox.GetObservable(TextBox.TextProperty) _textBox.GetObservable(TextBox.TextProperty)
.Skip(1)
.Subscribe(_ => OnTextBoxTextChanged()); .Subscribe(_ => OnTextBoxTextChanged());
if (Text != null) if (Text != null)
@ -1242,6 +1245,20 @@ namespace Avalonia.Controls
base.OnApplyTemplate(e); base.OnApplyTemplate(e);
} }
/// <summary>
/// Called to update the validation state for properties for which data validation is
/// enabled.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The new binding value for the property.</param>
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
if (property == TextProperty)
{
DataValidationErrors.SetError(this, value.Error);
}
}
/// <summary> /// <summary>
/// Provides handling for the /// Provides handling for the

12
src/Avalonia.Controls/Button.cs

@ -313,17 +313,13 @@ namespace Avalonia.Controls
IsPressed = false; IsPressed = false;
} }
protected override void OnPropertyChanged<T>( protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{ {
base.OnPropertyChanged(property, oldValue, newValue, priority); base.OnPropertyChanged(change);
if (property == IsPressedProperty) if (change.Property == IsPressedProperty)
{ {
UpdatePseudoClasses(newValue.GetValueOrDefault<bool>()); UpdatePseudoClasses(change.NewValue.GetValueOrDefault<bool>());
} }
} }

12
src/Avalonia.Controls/ButtonSpinner.cs

@ -205,17 +205,13 @@ namespace Avalonia.Controls
} }
} }
protected override void OnPropertyChanged<T>( protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{ {
base.OnPropertyChanged(property, oldValue, newValue, priority); base.OnPropertyChanged(change);
if (property == ButtonSpinnerLocationProperty) if (change.Property == ButtonSpinnerLocationProperty)
{ {
UpdatePseudoClasses(newValue.GetValueOrDefault<Location>()); UpdatePseudoClasses(change.NewValue.GetValueOrDefault<Location>());
} }
} }

4
src/Avalonia.Controls/Calendar/Calendar.cs

@ -998,10 +998,10 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Gets or sets a value indicating whether DatePicker should change its /// Gets or sets a value indicating whether CalendarDatePicker should change its
/// DisplayDate because of a SelectedDate change on its Calendar. /// DisplayDate because of a SelectedDate change on its Calendar.
/// </summary> /// </summary>
internal bool DatePickerDisplayDateFlag { get; set; } internal bool CalendarDatePickerDisplayDateFlag { get; set; }
internal CalendarDayButton FindDayButtonFromDay(DateTime day) internal CalendarDayButton FindDayButtonFromDay(DateTime day)
{ {

146
src/Avalonia.Controls/Calendar/DatePicker.cs → src/Avalonia.Controls/Calendar/CalendarDatePicker.cs

@ -16,29 +16,29 @@ namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Provides data for the /// Provides data for the
/// <see cref="E:Avalonia.Controls.DatePicker.DateValidationError" /> /// <see cref="E:Avalonia.Controls.CalendarDatePicker.DateValidationError" />
/// event. /// event.
/// </summary> /// </summary>
public class DatePickerDateValidationErrorEventArgs : EventArgs public class CalendarDatePickerDateValidationErrorEventArgs : EventArgs
{ {
private bool _throwException; private bool _throwException;
/// <summary> /// <summary>
/// Initializes a new instance of the /// Initializes a new instance of the
/// <see cref="T:Avalonia.Controls.DatePickerDateValidationErrorEventArgs" /> /// <see cref="T:Avalonia.Controls.CalendarDatePickerDateValidationErrorEventArgs" />
/// class. /// class.
/// </summary> /// </summary>
/// <param name="exception"> /// <param name="exception">
/// The initial exception from the /// The initial exception from the
/// <see cref="E:Avalonia.Controls.DatePicker.DateValidationError" /> /// <see cref="E:Avalonia.Controls.CalendarDatePicker.DateValidationError" />
/// event. /// event.
/// </param> /// </param>
/// <param name="text"> /// <param name="text">
/// The text that caused the /// The text that caused the
/// <see cref="E:Avalonia.Controls.DatePicker.DateValidationError" /> /// <see cref="E:Avalonia.Controls.CalendarDatePicker.DateValidationError" />
/// event. /// event.
/// </param> /// </param>
public DatePickerDateValidationErrorEventArgs(Exception exception, string text) public CalendarDatePickerDateValidationErrorEventArgs(Exception exception, string text)
{ {
this.Text = text; this.Text = text;
this.Exception = exception; this.Exception = exception;
@ -46,7 +46,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Gets the initial exception associated with the /// Gets the initial exception associated with the
/// <see cref="E:Avalonia.Controls.DatePicker.DateValidationError" /> /// <see cref="E:Avalonia.Controls.CalendarDatePicker.DateValidationError" />
/// event. /// event.
/// </summary> /// </summary>
/// <value> /// <value>
@ -56,7 +56,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Gets the text that caused the /// Gets the text that caused the
/// <see cref="E:Avalonia.Controls.DatePicker.DateValidationError" /> /// <see cref="E:Avalonia.Controls.CalendarDatePicker.DateValidationError" />
/// event. /// event.
/// </summary> /// </summary>
/// <value> /// <value>
@ -66,7 +66,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Gets or sets a value indicating whether /// Gets or sets a value indicating whether
/// <see cref="P:Avalonia.Controls.DatePickerDateValidationErrorEventArgs.Exception" /> /// <see cref="P:Avalonia.Controls.CalendarDatePickerDateValidationErrorEventArgs.Exception" />
/// should be thrown. /// should be thrown.
/// </summary> /// </summary>
/// <value> /// <value>
@ -74,7 +74,7 @@ namespace Avalonia.Controls
/// </value> /// </value>
/// <exception cref="T:System.ArgumentException"> /// <exception cref="T:System.ArgumentException">
/// If set to true and /// If set to true and
/// <see cref="P:Avalonia.Controls.DatePickerDateValidationErrorEventArgs.Exception" /> /// <see cref="P:Avalonia.Controls.CalendarDatePickerDateValidationErrorEventArgs.Exception" />
/// is null. /// is null.
/// </exception> /// </exception>
public bool ThrowException public bool ThrowException
@ -93,9 +93,9 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Specifies date formats for a /// Specifies date formats for a
/// <see cref="T:Avalonia.Controls.DatePicker" />. /// <see cref="T:Avalonia.Controls.CalendarDatePicker" />.
/// </summary> /// </summary>
public enum DatePickerFormat public enum CalendarDatePickerFormat
{ {
/// <summary> /// <summary>
/// Specifies that the date should be displayed using unabbreviated days /// Specifies that the date should be displayed using unabbreviated days
@ -115,7 +115,7 @@ namespace Avalonia.Controls
Custom = 2 Custom = 2
} }
public class DatePicker : TemplatedControl public class CalendarDatePicker : TemplatedControl
{ {
private const string ElementTextBox = "PART_TextBox"; private const string ElementTextBox = "PART_TextBox";
private const string ElementButton = "PART_Button"; private const string ElementButton = "PART_Button";
@ -154,59 +154,59 @@ namespace Avalonia.Controls
/// </value> /// </value>
public CalendarBlackoutDatesCollection BlackoutDates { get; private set; } public CalendarBlackoutDatesCollection BlackoutDates { get; private set; }
public static readonly DirectProperty<DatePicker, DateTime> DisplayDateProperty = public static readonly DirectProperty<CalendarDatePicker, DateTime> DisplayDateProperty =
AvaloniaProperty.RegisterDirect<DatePicker, DateTime>( AvaloniaProperty.RegisterDirect<CalendarDatePicker, DateTime>(
nameof(DisplayDate), nameof(DisplayDate),
o => o.DisplayDate, o => o.DisplayDate,
(o, v) => o.DisplayDate = v); (o, v) => o.DisplayDate = v);
public static readonly DirectProperty<DatePicker, DateTime?> DisplayDateStartProperty = public static readonly DirectProperty<CalendarDatePicker, DateTime?> DisplayDateStartProperty =
AvaloniaProperty.RegisterDirect<DatePicker, DateTime?>( AvaloniaProperty.RegisterDirect<CalendarDatePicker, DateTime?>(
nameof(DisplayDateStart), nameof(DisplayDateStart),
o => o.DisplayDateStart, o => o.DisplayDateStart,
(o, v) => o.DisplayDateStart = v); (o, v) => o.DisplayDateStart = v);
public static readonly DirectProperty<DatePicker, DateTime?> DisplayDateEndProperty = public static readonly DirectProperty<CalendarDatePicker, DateTime?> DisplayDateEndProperty =
AvaloniaProperty.RegisterDirect<DatePicker, DateTime?>( AvaloniaProperty.RegisterDirect<CalendarDatePicker, DateTime?>(
nameof(DisplayDateEnd), nameof(DisplayDateEnd),
o => o.DisplayDateEnd, o => o.DisplayDateEnd,
(o, v) => o.DisplayDateEnd = v); (o, v) => o.DisplayDateEnd = v);
public static readonly StyledProperty<DayOfWeek> FirstDayOfWeekProperty = public static readonly StyledProperty<DayOfWeek> FirstDayOfWeekProperty =
AvaloniaProperty.Register<DatePicker, DayOfWeek>(nameof(FirstDayOfWeek)); AvaloniaProperty.Register<CalendarDatePicker, DayOfWeek>(nameof(FirstDayOfWeek));
public static readonly DirectProperty<DatePicker, bool> IsDropDownOpenProperty = public static readonly DirectProperty<CalendarDatePicker, bool> IsDropDownOpenProperty =
AvaloniaProperty.RegisterDirect<DatePicker, bool>( AvaloniaProperty.RegisterDirect<CalendarDatePicker, bool>(
nameof(IsDropDownOpen), nameof(IsDropDownOpen),
o => o.IsDropDownOpen, o => o.IsDropDownOpen,
(o, v) => o.IsDropDownOpen = v); (o, v) => o.IsDropDownOpen = v);
public static readonly StyledProperty<bool> IsTodayHighlightedProperty = public static readonly StyledProperty<bool> IsTodayHighlightedProperty =
AvaloniaProperty.Register<DatePicker, bool>(nameof(IsTodayHighlighted)); AvaloniaProperty.Register<CalendarDatePicker, bool>(nameof(IsTodayHighlighted));
public static readonly DirectProperty<DatePicker, DateTime?> SelectedDateProperty = public static readonly DirectProperty<CalendarDatePicker, DateTime?> SelectedDateProperty =
AvaloniaProperty.RegisterDirect<DatePicker, DateTime?>( AvaloniaProperty.RegisterDirect<CalendarDatePicker, DateTime?>(
nameof(SelectedDate), nameof(SelectedDate),
o => o.SelectedDate, o => o.SelectedDate,
(o, v) => o.SelectedDate = v); (o, v) => o.SelectedDate = v);
public static readonly StyledProperty<DatePickerFormat> SelectedDateFormatProperty = public static readonly StyledProperty<CalendarDatePickerFormat> SelectedDateFormatProperty =
AvaloniaProperty.Register<DatePicker, DatePickerFormat>( AvaloniaProperty.Register<CalendarDatePicker, CalendarDatePickerFormat>(
nameof(SelectedDateFormat), nameof(SelectedDateFormat),
defaultValue: DatePickerFormat.Short, defaultValue: CalendarDatePickerFormat.Short,
validate: IsValidSelectedDateFormat); validate: IsValidSelectedDateFormat);
public static readonly StyledProperty<string> CustomDateFormatStringProperty = public static readonly StyledProperty<string> CustomDateFormatStringProperty =
AvaloniaProperty.Register<DatePicker, string>( AvaloniaProperty.Register<CalendarDatePicker, string>(
nameof(CustomDateFormatString), nameof(CustomDateFormatString),
defaultValue: "d", defaultValue: "d",
validate: IsValidDateFormatString); validate: IsValidDateFormatString);
public static readonly DirectProperty<DatePicker, string> TextProperty = public static readonly DirectProperty<CalendarDatePicker, string> TextProperty =
AvaloniaProperty.RegisterDirect<DatePicker, string>( AvaloniaProperty.RegisterDirect<CalendarDatePicker, string>(
nameof(Text), nameof(Text),
o => o.Text, o => o.Text,
(o, v) => o.Text = v); (o, v) => o.Text = v);
public static readonly StyledProperty<string> WatermarkProperty = public static readonly StyledProperty<string> WatermarkProperty =
TextBox.WatermarkProperty.AddOwner<DatePicker>(); TextBox.WatermarkProperty.AddOwner<CalendarDatePicker>();
public static readonly StyledProperty<bool> UseFloatingWatermarkProperty = public static readonly StyledProperty<bool> UseFloatingWatermarkProperty =
TextBox.UseFloatingWatermarkProperty.AddOwner<DatePicker>(); TextBox.UseFloatingWatermarkProperty.AddOwner<CalendarDatePicker>();
/// <summary> /// <summary>
@ -218,9 +218,9 @@ namespace Avalonia.Controls
/// </value> /// </value>
/// <exception cref="T:System.ArgumentOutOfRangeException"> /// <exception cref="T:System.ArgumentOutOfRangeException">
/// The specified date is not in the range defined by /// The specified date is not in the range defined by
/// <see cref="P:Avalonia.Controls.DatePicker.DisplayDateStart" /> /// <see cref="P:Avalonia.Controls.CalendarDatePicker.DisplayDateStart" />
/// and /// and
/// <see cref="P:Avalonia.Controls.DatePicker.DisplayDateEnd" />. /// <see cref="P:Avalonia.Controls.CalendarDatePicker.DisplayDateEnd" />.
/// </exception> /// </exception>
public DateTime DisplayDate public DateTime DisplayDate
{ {
@ -320,7 +320,7 @@ namespace Avalonia.Controls
/// <exception cref="T:System.ArgumentOutOfRangeException"> /// <exception cref="T:System.ArgumentOutOfRangeException">
/// An specified format is not valid. /// An specified format is not valid.
/// </exception> /// </exception>
public DatePickerFormat SelectedDateFormat public CalendarDatePickerFormat SelectedDateFormat
{ {
get { return GetValue(SelectedDateFormatProperty); } get { return GetValue(SelectedDateFormatProperty); }
set { SetValue(SelectedDateFormatProperty, value); } set { SetValue(SelectedDateFormatProperty, value); }
@ -380,33 +380,33 @@ namespace Avalonia.Controls
/// Occurs when <see cref="P:Avalonia.Controls.DatePicker.Text" /> /// Occurs when <see cref="P:Avalonia.Controls.DatePicker.Text" />
/// is assigned a value that cannot be interpreted as a date. /// is assigned a value that cannot be interpreted as a date.
/// </summary> /// </summary>
public event EventHandler<DatePickerDateValidationErrorEventArgs> DateValidationError; public event EventHandler<CalendarDatePickerDateValidationErrorEventArgs> DateValidationError;
/// <summary> /// <summary>
/// Occurs when the /// Occurs when the
/// <see cref="P:Avalonia.Controls.DatePicker.SelectedDate" /> /// <see cref="P:Avalonia.Controls.CalendarDatePicker.SelectedDate" />
/// property is changed. /// property is changed.
/// </summary> /// </summary>
public event EventHandler<SelectionChangedEventArgs> SelectedDateChanged; public event EventHandler<SelectionChangedEventArgs> SelectedDateChanged;
static DatePicker() static CalendarDatePicker()
{ {
FocusableProperty.OverrideDefaultValue<DatePicker>(true); FocusableProperty.OverrideDefaultValue<CalendarDatePicker>(true);
DisplayDateProperty.Changed.AddClassHandler<DatePicker>((x,e) => x.OnDisplayDateChanged(e)); DisplayDateProperty.Changed.AddClassHandler<CalendarDatePicker>((x,e) => x.OnDisplayDateChanged(e));
DisplayDateStartProperty.Changed.AddClassHandler<DatePicker>((x,e) => x.OnDisplayDateStartChanged(e)); DisplayDateStartProperty.Changed.AddClassHandler<CalendarDatePicker>((x,e) => x.OnDisplayDateStartChanged(e));
DisplayDateEndProperty.Changed.AddClassHandler<DatePicker>((x,e) => x.OnDisplayDateEndChanged(e)); DisplayDateEndProperty.Changed.AddClassHandler<CalendarDatePicker>((x,e) => x.OnDisplayDateEndChanged(e));
IsDropDownOpenProperty.Changed.AddClassHandler<DatePicker>((x,e) => x.OnIsDropDownOpenChanged(e)); IsDropDownOpenProperty.Changed.AddClassHandler<CalendarDatePicker>((x,e) => x.OnIsDropDownOpenChanged(e));
SelectedDateProperty.Changed.AddClassHandler<DatePicker>((x,e) => x.OnSelectedDateChanged(e)); SelectedDateProperty.Changed.AddClassHandler<CalendarDatePicker>((x,e) => x.OnSelectedDateChanged(e));
SelectedDateFormatProperty.Changed.AddClassHandler<DatePicker>((x,e) => x.OnSelectedDateFormatChanged(e)); SelectedDateFormatProperty.Changed.AddClassHandler<CalendarDatePicker>((x,e) => x.OnSelectedDateFormatChanged(e));
CustomDateFormatStringProperty.Changed.AddClassHandler<DatePicker>((x,e) => x.OnCustomDateFormatStringChanged(e)); CustomDateFormatStringProperty.Changed.AddClassHandler<CalendarDatePicker>((x,e) => x.OnCustomDateFormatStringChanged(e));
TextProperty.Changed.AddClassHandler<DatePicker>((x,e) => x.OnTextChanged(e)); TextProperty.Changed.AddClassHandler<CalendarDatePicker>((x,e) => x.OnTextChanged(e));
} }
/// <summary> /// <summary>
/// Initializes a new instance of the /// Initializes a new instance of the
/// <see cref="T:Avalonia.Controls.DatePicker" /> class. /// <see cref="T:Avalonia.Controls.DatePicker" /> class.
/// </summary> /// </summary>
public DatePicker() public CalendarDatePicker()
{ {
FirstDayOfWeek = DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek; FirstDayOfWeek = DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek;
_defaultText = string.Empty; _defaultText = string.Empty;
@ -510,17 +510,13 @@ namespace Avalonia.Controls
} }
} }
protected override void OnPropertyChanged<T>( protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{ {
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);
} }
} }
@ -666,12 +662,12 @@ namespace Avalonia.Controls
// change is coming from the Calendar UI itself, so, we // change is coming from the Calendar UI itself, so, we
// shouldn't change the DisplayDate since it will automatically // shouldn't change the DisplayDate since it will automatically
// be changed by the Calendar // be changed by the Calendar
if ((day.Month != DisplayDate.Month || day.Year != DisplayDate.Year) && (_calendar == null || !_calendar.DatePickerDisplayDateFlag)) if ((day.Month != DisplayDate.Month || day.Year != DisplayDate.Year) && (_calendar == null || !_calendar.CalendarDatePickerDisplayDateFlag))
{ {
DisplayDate = day; DisplayDate = day;
} }
if(_calendar != null) if(_calendar != null)
_calendar.DatePickerDisplayDateFlag = false; _calendar.CalendarDatePickerDisplayDateFlag = false;
} }
else else
{ {
@ -711,7 +707,7 @@ namespace Avalonia.Controls
} }
private void OnCustomDateFormatStringChanged(AvaloniaPropertyChangedEventArgs e) private void OnCustomDateFormatStringChanged(AvaloniaPropertyChangedEventArgs e)
{ {
if(SelectedDateFormat == DatePickerFormat.Custom) if(SelectedDateFormat == CalendarDatePickerFormat.Custom)
{ {
OnDateFormatChanged(); OnDateFormatChanged();
} }
@ -756,15 +752,15 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Raises the /// Raises the
/// <see cref="E:Avalonia.Controls.DatePicker.DateValidationError" /> /// <see cref="E:Avalonia.Controls.CalendarDatePicker.DateValidationError" />
/// event. /// event.
/// </summary> /// </summary>
/// <param name="e"> /// <param name="e">
/// A /// A
/// <see cref="T:Avalonia.Controls.DatePickerDateValidationErrorEventArgs" /> /// <see cref="T:Avalonia.Controls.CalendarDatePickerDateValidationErrorEventArgs" />
/// that contains the event data. /// that contains the event data.
/// </param> /// </param>
protected virtual void OnDateValidationError(DatePickerDateValidationErrorEventArgs e) protected virtual void OnDateValidationError(CalendarDatePickerDateValidationErrorEventArgs e)
{ {
DateValidationError?.Invoke(this, e); DateValidationError?.Invoke(this, e);
} }
@ -963,7 +959,7 @@ namespace Avalonia.Controls
} }
else else
{ {
var dateValidationError = new DatePickerDateValidationErrorEventArgs(new ArgumentOutOfRangeException(nameof(text), "SelectedDate value is not valid."), text); var dateValidationError = new CalendarDatePickerDateValidationErrorEventArgs(new ArgumentOutOfRangeException(nameof(text), "SelectedDate value is not valid."), text);
OnDateValidationError(dateValidationError); OnDateValidationError(dateValidationError);
if (dateValidationError.ThrowException) if (dateValidationError.ThrowException)
@ -974,7 +970,7 @@ namespace Avalonia.Controls
} }
catch (FormatException ex) catch (FormatException ex)
{ {
DatePickerDateValidationErrorEventArgs textParseError = new DatePickerDateValidationErrorEventArgs(ex, text); CalendarDatePickerDateValidationErrorEventArgs textParseError = new CalendarDatePickerDateValidationErrorEventArgs(ex, text);
OnDateValidationError(textParseError); OnDateValidationError(textParseError);
if (textParseError.ThrowException) if (textParseError.ThrowException)
@ -990,11 +986,11 @@ namespace Avalonia.Controls
switch (SelectedDateFormat) switch (SelectedDateFormat)
{ {
case DatePickerFormat.Short: case CalendarDatePickerFormat.Short:
return string.Format(CultureInfo.CurrentCulture, d.ToString(dtfi.ShortDatePattern, dtfi)); return string.Format(CultureInfo.CurrentCulture, d.ToString(dtfi.ShortDatePattern, dtfi));
case DatePickerFormat.Long: case CalendarDatePickerFormat.Long:
return string.Format(CultureInfo.CurrentCulture, d.ToString(dtfi.LongDatePattern, dtfi)); return string.Format(CultureInfo.CurrentCulture, d.ToString(dtfi.LongDatePattern, dtfi));
case DatePickerFormat.Custom: case CalendarDatePickerFormat.Custom:
return string.Format(CultureInfo.CurrentCulture, d.ToString(CustomDateFormatString, dtfi)); return string.Format(CultureInfo.CurrentCulture, d.ToString(CustomDateFormatString, dtfi));
} }
return null; return null;
@ -1122,12 +1118,12 @@ namespace Avalonia.Controls
switch (SelectedDateFormat) switch (SelectedDateFormat)
{ {
case DatePickerFormat.Long: case CalendarDatePickerFormat.Long:
{ {
watermarkText = string.Format(CultureInfo.CurrentCulture, watermarkFormat, dtfi.LongDatePattern.ToString()); watermarkText = string.Format(CultureInfo.CurrentCulture, watermarkFormat, dtfi.LongDatePattern.ToString());
break; break;
} }
case DatePickerFormat.Short: case CalendarDatePickerFormat.Short:
default: default:
{ {
watermarkText = string.Format(CultureInfo.CurrentCulture, watermarkFormat, dtfi.ShortDatePattern.ToString()); watermarkText = string.Format(CultureInfo.CurrentCulture, watermarkFormat, dtfi.ShortDatePattern.ToString());
@ -1143,11 +1139,11 @@ namespace Avalonia.Controls
} }
} }
private static bool IsValidSelectedDateFormat(DatePickerFormat value) private static bool IsValidSelectedDateFormat(CalendarDatePickerFormat value)
{ {
return value == DatePickerFormat.Long return value == CalendarDatePickerFormat.Long
|| value == DatePickerFormat.Short || value == CalendarDatePickerFormat.Short
|| value == DatePickerFormat.Custom; || value == CalendarDatePickerFormat.Custom;
} }
private static bool IsValidDateFormatString(string formatString) private static bool IsValidDateFormatString(string formatString)
{ {

4
src/Avalonia.Controls/Calendar/CalendarItem.cs

@ -909,7 +909,7 @@ namespace Avalonia.Controls.Primitives
case CalendarSelectionMode.SingleDate: case CalendarSelectionMode.SingleDate:
{ {
DateTime selectedDate = (DateTime)b.DataContext; DateTime selectedDate = (DateTime)b.DataContext;
Owner.DatePickerDisplayDateFlag = true; Owner.CalendarDatePickerDisplayDateFlag = true;
if (Owner.SelectedDates.Count == 0) if (Owner.SelectedDates.Count == 0)
{ {
Owner.SelectedDates.Add(selectedDate); Owner.SelectedDates.Add(selectedDate);
@ -981,7 +981,7 @@ namespace Avalonia.Controls.Primitives
} }
case CalendarSelectionMode.SingleDate: case CalendarSelectionMode.SingleDate:
{ {
Owner.DatePickerDisplayDateFlag = true; Owner.CalendarDatePickerDisplayDateFlag = true;
if (Owner.SelectedDates.Count == 0) if (Owner.SelectedDates.Count == 0)
{ {
Owner.SelectedDates.Add(selectedDate); Owner.SelectedDates.Add(selectedDate);

30
src/Avalonia.Controls/ComboBox.cs

@ -51,6 +51,18 @@ namespace Avalonia.Controls
public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty = public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty =
ItemsPresenter.VirtualizationModeProperty.AddOwner<ComboBox>(); ItemsPresenter.VirtualizationModeProperty.AddOwner<ComboBox>();
/// <summary>
/// Defines the <see cref="PlaceholderText"/> property.
/// </summary>
public static readonly StyledProperty<string> PlaceholderTextProperty =
AvaloniaProperty.Register<ComboBox, string>(nameof(PlaceholderText));
/// <summary>
/// Defines the <see cref="PlaceholderForeground"/> property.
/// </summary>
public static readonly StyledProperty<IBrush> PlaceholderForegroundProperty =
AvaloniaProperty.Register<ComboBox, IBrush>(nameof(PlaceholderForeground));
private bool _isDropDownOpen; private bool _isDropDownOpen;
private Popup _popup; private Popup _popup;
private object _selectionBoxItem; private object _selectionBoxItem;
@ -94,6 +106,24 @@ namespace Avalonia.Controls
set { SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); } set { SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); }
} }
/// <summary>
/// Gets or sets the PlaceHolder text.
/// </summary>
public string PlaceholderText
{
get { return GetValue(PlaceholderTextProperty); }
set { SetValue(PlaceholderTextProperty, value); }
}
/// <summary>
/// Gets or sets the Brush that renders the placeholder text.
/// </summary>
public IBrush PlaceholderForeground
{
get { return GetValue(PlaceholderForegroundProperty); }
set { SetValue(PlaceholderForegroundProperty, value); }
}
/// <summary> /// <summary>
/// Gets or sets the virtualization mode for the items. /// Gets or sets the virtualization mode for the items.
/// </summary> /// </summary>

202
src/Avalonia.Controls/ContextMenu.cs

@ -1,27 +1,78 @@
using System; using System;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.LogicalTree; using Avalonia.Styling;
#nullable enable
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// A control context menu. /// A control context menu.
/// </summary> /// </summary>
public class ContextMenu : MenuBase public class ContextMenu : MenuBase, ISetterValue
{ {
/// <summary>
/// Defines the <see cref="HorizontalOffset"/> property.
/// </summary>
public static readonly StyledProperty<double> HorizontalOffsetProperty =
Popup.HorizontalOffsetProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="VerticalOffset"/> property.
/// </summary>
public static readonly StyledProperty<double> VerticalOffsetProperty =
Popup.VerticalOffsetProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="PlacementAnchor"/> property.
/// </summary>
public static readonly StyledProperty<PopupAnchor> PlacementAnchorProperty =
Popup.PlacementAnchorProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="PlacementConstraintAdjustment"/> property.
/// </summary>
public static readonly StyledProperty<PopupPositionerConstraintAdjustment> PlacementConstraintAdjustmentProperty =
Popup.PlacementConstraintAdjustmentProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="PlacementGravity"/> property.
/// </summary>
public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
Popup.PlacementGravityProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="PlacementMode"/> property.
/// </summary>
public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
Popup.PlacementModeProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="PlacementRect"/> property.
/// </summary>
public static readonly StyledProperty<Rect?> PlacementRectProperty =
AvaloniaProperty.Register<Popup, Rect?>(nameof(PlacementRect));
/// <summary>
/// Defines the <see cref="PlacementTarget"/> property.
/// </summary>
public static readonly StyledProperty<Control?> PlacementTargetProperty =
Popup.PlacementTargetProperty.AddOwner<ContextMenu>();
private static readonly ITemplate<IPanel> DefaultPanel = private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Vertical }); new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Vertical });
private Popup _popup; private Popup? _popup;
private Control _attachedControl; private List<Control>? _attachedControls;
private IInputElement _previousFocus; private IInputElement? _previousFocus;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ContextMenu"/> class. /// Initializes a new instance of the <see cref="ContextMenu"/> class.
@ -45,23 +96,107 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
static ContextMenu() static ContextMenu()
{ {
ItemsPanelProperty.OverrideDefaultValue(typeof(ContextMenu), DefaultPanel); ItemsPanelProperty.OverrideDefaultValue<ContextMenu>(DefaultPanel);
PlacementModeProperty.OverrideDefaultValue<ContextMenu>(PlacementMode.Pointer);
ContextMenuProperty.Changed.Subscribe(ContextMenuChanged); ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
} }
/// <summary>
/// Gets or sets the Horizontal offset of the context menu in relation to the <see cref="PlacementTarget"/>.
/// </summary>
public double HorizontalOffset
{
get { return GetValue(HorizontalOffsetProperty); }
set { SetValue(HorizontalOffsetProperty, value); }
}
/// <summary>
/// Gets or sets the Vertical offset of the context menu in relation to the <see cref="PlacementTarget"/>.
/// </summary>
public double VerticalOffset
{
get { return GetValue(VerticalOffsetProperty); }
set { SetValue(VerticalOffsetProperty, value); }
}
/// <summary>
/// Gets or sets the anchor point on the <see cref="PlacementRect"/> when <see cref="PlacementMode"/>
/// is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
public PopupAnchor PlacementAnchor
{
get { return GetValue(PlacementAnchorProperty); }
set { SetValue(PlacementAnchorProperty, value); }
}
/// <summary>
/// Gets or sets a value describing how the context menu position will be adjusted if the
/// unadjusted position would result in the context menu being partly constrained.
/// </summary>
public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
{
get { return GetValue(PlacementConstraintAdjustmentProperty); }
set { SetValue(PlacementConstraintAdjustmentProperty, value); }
}
/// <summary>
/// Gets or sets a value which defines in what direction the context menu should open
/// when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
public PopupGravity PlacementGravity
{
get { return GetValue(PlacementGravityProperty); }
set { SetValue(PlacementGravityProperty, value); }
}
/// <summary>
/// Gets or sets the placement mode of the context menu in relation to the<see cref="PlacementTarget"/>.
/// </summary>
public PlacementMode PlacementMode
{
get { return GetValue(PlacementModeProperty); }
set { SetValue(PlacementModeProperty, value); }
}
/// <summary>
/// Gets or sets the the anchor rectangle within the parent that the context menu will be placed
/// relative to when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
/// <remarks>
/// The placement rect defines a rectangle relative to <see cref="PlacementTarget"/> around
/// which the popup will be opened, with <see cref="PlacementAnchor"/> determining which edge
/// of the placement target is used.
///
/// If unset, the anchor rectangle will be the bounds of the <see cref="PlacementTarget"/>.
/// </remarks>
public Rect? PlacementRect
{
get { return GetValue(PlacementRectProperty); }
set { SetValue(PlacementRectProperty, value); }
}
/// <summary>
/// Gets or sets the control that is used to determine the popup's position.
/// </summary>
public Control? PlacementTarget
{
get { return GetValue(PlacementTargetProperty); }
set { SetValue(PlacementTargetProperty, value); }
}
/// <summary> /// <summary>
/// Occurs when the value of the /// Occurs when the value of the
/// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" /> /// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" />
/// property is changing from false to true. /// property is changing from false to true.
/// </summary> /// </summary>
public event CancelEventHandler ContextMenuOpening; public event CancelEventHandler? ContextMenuOpening;
/// <summary> /// <summary>
/// Occurs when the value of the /// Occurs when the value of the
/// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" /> /// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" />
/// property is changing from true to false. /// property is changing from true to false.
/// </summary> /// </summary>
public event CancelEventHandler ContextMenuClosing; public event CancelEventHandler? ContextMenuClosing;
/// <summary> /// <summary>
/// Called when the <see cref="Control.ContextMenu"/> property changes on a control. /// Called when the <see cref="Control.ContextMenu"/> property changes on a control.
@ -74,13 +209,14 @@ namespace Avalonia.Controls
if (e.OldValue is ContextMenu oldMenu) if (e.OldValue is ContextMenu oldMenu)
{ {
control.PointerReleased -= ControlPointerReleased; control.PointerReleased -= ControlPointerReleased;
oldMenu._attachedControl = null; oldMenu._attachedControls?.Remove(control);
((ISetLogicalParent)oldMenu._popup)?.SetParent(null); ((ISetLogicalParent?)oldMenu._popup)?.SetParent(null);
} }
if (e.NewValue is ContextMenu newMenu) if (e.NewValue is ContextMenu newMenu)
{ {
newMenu._attachedControl = control; newMenu._attachedControls ??= new List<Control>();
newMenu._attachedControls.Add(control);
control.PointerReleased += ControlPointerReleased; control.PointerReleased += ControlPointerReleased;
} }
} }
@ -94,20 +230,24 @@ namespace Avalonia.Controls
/// Opens a context menu on the specified control. /// Opens a context menu on the specified control.
/// </summary> /// </summary>
/// <param name="control">The control.</param> /// <param name="control">The control.</param>
public void Open(Control 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)); 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( throw new ArgumentException(
"Cannot show ContentMenu on a different control to the one it is attached to.", "Cannot show ContentMenu on a different control to the one it is attached to.",
nameof(control)); nameof(control));
} }
control ??= _attachedControls![0];
if (IsOpen) if (IsOpen)
{ {
return; return;
@ -117,8 +257,14 @@ namespace Avalonia.Controls
{ {
_popup = new Popup _popup = new Popup
{ {
PlacementMode = PlacementMode.Pointer, HorizontalOffset = HorizontalOffset,
PlacementTarget = control, VerticalOffset = VerticalOffset,
PlacementAnchor = PlacementAnchor,
PlacementConstraintAdjustment = PlacementConstraintAdjustment,
PlacementGravity = PlacementGravity,
PlacementMode = PlacementMode,
PlacementRect = PlacementRect,
PlacementTarget = PlacementTarget ?? control,
StaysOpen = false StaysOpen = false
}; };
@ -126,7 +272,12 @@ namespace Avalonia.Controls
_popup.Closed += PopupClosed; _popup.Closed += PopupClosed;
} }
((ISetLogicalParent)_popup).SetParent(control); if (_popup.Parent != control)
{
((ISetLogicalParent)_popup).SetParent(null);
((ISetLogicalParent)_popup).SetParent(control);
}
_popup.Child = this; _popup.Child = this;
_popup.IsOpen = true; _popup.IsOpen = true;
@ -155,6 +306,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 <template>.
if (!(setter is Setter s && s.Property == ContextMenuProperty))
{
throw new InvalidOperationException(
"Cannot use a control as a Setter value. Wrap the control in a <Template>.");
}
}
protected override IItemContainerGenerator CreateItemContainerGenerator() protected override IItemContainerGenerator CreateItemContainerGenerator()
{ {
return new MenuItemContainerGenerator(this); return new MenuItemContainerGenerator(this);
@ -179,9 +341,9 @@ namespace Avalonia.Controls
SelectedIndex = -1; SelectedIndex = -1;
IsOpen = false; IsOpen = false;
if (_attachedControl is null) if (_attachedControls is null || _attachedControls.Count == 0)
{ {
((ISetLogicalParent)_popup).SetParent(null); ((ISetLogicalParent)_popup!).SetParent(null);
} }
// HACK: Reset the focus when the popup is closed. We need to fix this so it's automatic. // HACK: Reset the focus when the popup is closed. We need to fix this so it's automatic.

22
src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs

@ -18,10 +18,24 @@ namespace Avalonia.Controls.Converters
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{ {
if (!(value is int depth)) if (value is int scalarDepth)
return new Thickness(0); {
return new Thickness(
return new Thickness(Left ? Indent * depth : 0, Top ? Indent * depth : 0, Right ? Indent * depth : 0, Bottom ? Indent * depth : 0); Left ? Indent * scalarDepth : 0,
Top ? Indent * scalarDepth : 0,
Right ? Indent * scalarDepth : 0,
Bottom ? Indent * scalarDepth : 0);
}
else if (value is Thickness thinknessDepth)
{
return new Thickness(
Left ? Indent * thinknessDepth.Left : 0,
Top ? Indent * thinknessDepth.Top : 0,
Right ? Indent * thinknessDepth.Right : 0,
Bottom ? Indent * thinknessDepth.Bottom : 0);
}
return new Thickness(0);
} }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)

64
src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Controls.Primitives;
using Avalonia.Data.Converters;
using Avalonia.Utilities;
namespace Avalonia.Controls.Converters
{
public class MenuScrollingVisibilityConverter : IMultiValueConverter
{
public static readonly MenuScrollingVisibilityConverter Instance = new MenuScrollingVisibilityConverter();
public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
{
if (parameter == null ||
values == null ||
values.Count != 4 ||
!(values[0] is ScrollBarVisibility visiblity) ||
!(values[1] is double offset) ||
!(values[2] is double extent) ||
!(values[3] is double viewport))
{
return AvaloniaProperty.UnsetValue;
}
if (visiblity == ScrollBarVisibility.Auto)
{
if (extent == viewport)
{
return false;
}
double target;
if (parameter is double d)
{
target = d;
}
else if (parameter is string s)
{
target = double.Parse(s, NumberFormatInfo.InvariantInfo);
}
else
{
return AvaloniaProperty.UnsetValue;
}
// Calculate the percent so that we can see if we are near the edge of the range
double percent = MathUtilities.Clamp(offset * 100.0 / (extent - viewport), 0, 100);
if (MathUtilities.AreClose(percent, target))
{
// We are at the end of the range, so no need for this button to be shown
return false;
}
return true;
}
return false;
}
}
}

8
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

@ -49,6 +49,9 @@ namespace Avalonia.Controls.Embedding.Offscreen
public Action<Rect> Paint { get; set; } public Action<Rect> Paint { get; set; }
public Action<Size> Resized { get; set; } public Action<Size> Resized { get; set; }
public Action<double> ScalingChanged { get; set; } public Action<double> ScalingChanged { get; set; }
public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; }
public void SetInputRoot(IInputRoot inputRoot) => InputRoot = inputRoot; public void SetInputRoot(IInputRoot inputRoot) => InputRoot = inputRoot;
public virtual Point PointToClient(PixelPoint point) => point.ToPoint(1); public virtual Point PointToClient(PixelPoint point) => point.ToPoint(1);
@ -61,6 +64,11 @@ namespace Avalonia.Controls.Embedding.Offscreen
public Action Closed { get; set; } public Action Closed { get; set; }
public abstract IMouseDevice MouseDevice { get; } public abstract IMouseDevice MouseDevice { get; }
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { }
public WindowTransparencyLevel TransparencyLevel { get; private set; }
public IPopupImpl CreatePopup() => null; public IPopupImpl CreatePopup() => null;
} }
} }

12
src/Avalonia.Controls/Expander.cs

@ -78,17 +78,13 @@ namespace Avalonia.Controls
} }
} }
protected override void OnPropertyChanged<T>( protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{ {
base.OnPropertyChanged(property, oldValue, newValue, priority); base.OnPropertyChanged(change);
if (property == ExpandDirectionProperty) if (change.Property == ExpandDirectionProperty)
{ {
UpdatePseudoClasses(newValue.GetValueOrDefault<ExpandDirection>()); UpdatePseudoClasses(change.NewValue.GetValueOrDefault<ExpandDirection>());
} }
} }

35
src/Avalonia.Controls/Grid.cs

@ -1228,7 +1228,7 @@ namespace Avalonia.Controls
Debug.Assert(1 < count && 0 <= start && (start + count) <= definitions.Count); Debug.Assert(1 < count && 0 <= start && (start + count) <= definitions.Count);
// avoid processing when asked to distribute "0" // avoid processing when asked to distribute "0"
if (!_IsZero(requestedSize)) if (!MathUtilities.IsZero(requestedSize))
{ {
DefinitionBase[] tempDefinitions = TempDefinitions; // temp array used to remember definitions for sorting DefinitionBase[] tempDefinitions = TempDefinitions; // temp array used to remember definitions for sorting
int end = start + count; int end = start + count;
@ -1306,7 +1306,7 @@ namespace Avalonia.Controls
} }
// sanity check: requested size must all be distributed // sanity check: requested size must all be distributed
Debug.Assert(_IsZero(sizeToDistribute)); Debug.Assert(MathUtilities.IsZero(sizeToDistribute));
} }
else if (requestedSize <= rangeMaxSize) else if (requestedSize <= rangeMaxSize)
{ {
@ -1346,7 +1346,7 @@ namespace Avalonia.Controls
} }
// sanity check: requested size must all be distributed // sanity check: requested size must all be distributed
Debug.Assert(_IsZero(sizeToDistribute)); Debug.Assert(MathUtilities.IsZero(sizeToDistribute));
} }
else else
{ {
@ -1358,7 +1358,7 @@ namespace Avalonia.Controls
double equalSize = requestedSize / count; double equalSize = requestedSize / count;
if (equalSize < maxMaxSize if (equalSize < maxMaxSize
&& !_AreClose(equalSize, maxMaxSize)) && !MathUtilities.AreClose(equalSize, maxMaxSize))
{ {
// equi-size is less than maximum of maxSizes. // equi-size is less than maximum of maxSizes.
// in this case distribute so that smaller definitions grow faster than // in this case distribute so that smaller definitions grow faster than
@ -2151,7 +2151,7 @@ namespace Avalonia.Controls
// and precision of floating-point computation. (However, the resulting // and precision of floating-point computation. (However, the resulting
// display is subject to anti-aliasing problems. TANSTAAFL.) // display is subject to anti-aliasing problems. TANSTAAFL.)
if (!_AreClose(roundedTakenSize, finalSize)) if (!MathUtilities.AreClose(roundedTakenSize, finalSize))
{ {
// Compute deltas // Compute deltas
for (int i = 0; i < definitions.Count; ++i) for (int i = 0; i < definitions.Count; ++i)
@ -2168,7 +2168,7 @@ namespace Avalonia.Controls
if (roundedTakenSize > finalSize) if (roundedTakenSize > finalSize)
{ {
int i = definitions.Count - 1; int i = definitions.Count - 1;
while ((adjustedSize > finalSize && !_AreClose(adjustedSize, finalSize)) && i >= 0) while ((adjustedSize > finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i >= 0)
{ {
DefinitionBase definition = definitions[definitionIndices[i]]; DefinitionBase definition = definitions[definitionIndices[i]];
double final = definition.SizeCache - dpiIncrement; double final = definition.SizeCache - dpiIncrement;
@ -2184,7 +2184,7 @@ namespace Avalonia.Controls
else if (roundedTakenSize < finalSize) else if (roundedTakenSize < finalSize)
{ {
int i = 0; int i = 0;
while ((adjustedSize < finalSize && !_AreClose(adjustedSize, finalSize)) && i < definitions.Count) while ((adjustedSize < finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i < definitions.Count)
{ {
DefinitionBase definition = definitions[definitionIndices[i]]; DefinitionBase definition = definitions[definitionIndices[i]];
double final = definition.SizeCache + dpiIncrement; double final = definition.SizeCache + dpiIncrement;
@ -2595,27 +2595,6 @@ namespace Avalonia.Controls
set { SetFlags(value, Flags.HasGroup3CellsInAutoRows); } set { SetFlags(value, Flags.HasGroup3CellsInAutoRows); }
} }
/// <summary>
/// fp version of <c>d == 0</c>.
/// </summary>
/// <param name="d">Value to check.</param>
/// <returns><c>true</c> if d == 0.</returns>
private static bool _IsZero(double d)
{
return (Math.Abs(d) < double.Epsilon);
}
/// <summary>
/// fp version of <c>d1 == d2</c>
/// </summary>
/// <param name="d1">First value to compare</param>
/// <param name="d2">Second value to compare</param>
/// <returns><c>true</c> if d1 == d2</returns>
private static bool _AreClose(double d1, double d2)
{
return (Math.Abs(d1 - d2) < double.Epsilon);
}
/// <summary> /// <summary>
/// Returns reference to extended data bag. /// Returns reference to extended data bag.
/// </summary> /// </summary>

20
src/Avalonia.Controls/IndexPath.cs

@ -123,6 +123,26 @@ namespace Avalonia.Controls
} }
} }
public bool IsAncestorOf(in IndexPath other)
{
if (other.GetSize() <= GetSize())
{
return false;
}
var size = GetSize();
for (int i = 0; i < size; i++)
{
if (GetAt(i) != other.GetAt(i))
{
return false;
}
}
return true;
}
public override string ToString() public override string ToString()
{ {
if (_path != null) if (_path != null)

47
src/Avalonia.Controls/IndexRange.cs

@ -132,6 +132,53 @@ namespace Avalonia.Controls
return result; return result;
} }
public static int Intersect(
IList<IndexRange> ranges,
IndexRange range,
IList<IndexRange>? removed = null)
{
var result = 0;
for (var i = 0; i < ranges.Count && range != s_invalid; ++i)
{
var existing = ranges[i];
if (existing.End < range.Begin || existing.Begin > range.End)
{
removed?.Add(existing);
ranges.RemoveAt(i--);
result += existing.Count;
}
else
{
if (existing.Begin < range.Begin)
{
var except = new IndexRange(existing.Begin, range.Begin - 1);
removed?.Add(except);
ranges[i] = existing = new IndexRange(range.Begin, existing.End);
result += except.Count;
}
if (existing.End > range.End)
{
var except = new IndexRange(range.End + 1, existing.End);
removed?.Add(except);
ranges[i] = new IndexRange(existing.Begin, range.End);
result += except.Count;
}
}
}
MergeRanges(ranges);
if (removed is object)
{
MergeRanges(removed);
}
return result;
}
public static int Remove( public static int Remove(
IList<IndexRange> ranges, IList<IndexRange> ranges,
IndexRange range, IndexRange range,

6
src/Avalonia.Controls/LayoutTransformControl.cs

@ -14,8 +14,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public class LayoutTransformControl : Decorator public class LayoutTransformControl : Decorator
{ {
public static readonly StyledProperty<Transform> LayoutTransformProperty = public static readonly StyledProperty<ITransform> LayoutTransformProperty =
AvaloniaProperty.Register<LayoutTransformControl, Transform>(nameof(LayoutTransform)); AvaloniaProperty.Register<LayoutTransformControl, ITransform>(nameof(LayoutTransform));
public static readonly StyledProperty<bool> UseRenderTransformProperty = public static readonly StyledProperty<bool> UseRenderTransformProperty =
AvaloniaProperty.Register<LayoutTransformControl, bool>(nameof(LayoutTransform)); AvaloniaProperty.Register<LayoutTransformControl, bool>(nameof(LayoutTransform));
@ -37,7 +37,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Gets or sets a graphics transformation that should apply to this element when layout is performed. /// Gets or sets a graphics transformation that should apply to this element when layout is performed.
/// </summary> /// </summary>
public Transform LayoutTransform public ITransform LayoutTransform
{ {
get { return GetValue(LayoutTransformProperty); } get { return GetValue(LayoutTransformProperty); }
set { SetValue(LayoutTransformProperty, value); } set { SetValue(LayoutTransformProperty, value); }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save