Browse Source

Merge branch 'master' into treeview-bugfix

pull/5858/head
Steven Kirk 5 years ago
committed by GitHub
parent
commit
6b164b6833
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/PULL_REQUEST_TEMPLATE.md
  2. 22
      Documentation/build.md
  3. 27
      native/Avalonia.Native/src/OSX/window.mm
  4. 4
      readme.md
  5. 8
      samples/ControlCatalog/App.xaml.cs
  6. 5
      samples/ControlCatalog/MainWindow.xaml.cs
  7. 6
      samples/ControlCatalog/Pages/ContextFlyoutPage.axaml
  8. 76
      samples/ControlCatalog/Pages/ViewboxPage.xaml
  9. 21
      samples/ControlCatalog/Pages/ViewboxPage.xaml.cs
  10. 3
      samples/RenderDemo/MainWindow.xaml
  11. 7
      samples/RenderDemo/Pages/AnimationsPage.xaml
  12. 25
      samples/RenderDemo/Pages/CustomAnimatorPage.xaml
  13. 27
      samples/RenderDemo/Pages/CustomAnimatorPage.xaml.cs
  14. 16
      samples/RenderDemo/Pages/CustomStringAnimator.cs
  15. 101
      samples/RenderDemo/Pages/TransitionsPage.xaml
  16. 70
      src/Avalonia.Animation/Animation.cs
  17. 25
      src/Avalonia.Animation/AnimationInstance`1.cs
  18. 20
      src/Avalonia.Animation/AnimatorDrivenTransition.cs
  19. 32
      src/Avalonia.Animation/AnimatorTransitionObservable.cs
  20. 8
      src/Avalonia.Animation/Animators/Animator`1.cs
  21. 6
      src/Avalonia.Animation/ApiCompatBaseline.txt
  22. 9
      src/Avalonia.Animation/Clock.cs
  23. 12
      src/Avalonia.Animation/ClockBase.cs
  24. 3
      src/Avalonia.Animation/IAnimation.cs
  25. 6
      src/Avalonia.Animation/Transition.cs
  26. 60
      src/Avalonia.Animation/TransitionInstance.cs
  27. 58
      src/Avalonia.Animation/TransitionObservableBase.cs
  28. 15
      src/Avalonia.Animation/Transitions/DoubleTransition.cs
  29. 12
      src/Avalonia.Animation/Transitions/FloatTransition.cs
  30. 12
      src/Avalonia.Animation/Transitions/IntegerTransition.cs
  31. 39
      src/Avalonia.Base/AvaloniaObject.cs
  32. 24
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  33. 2
      src/Avalonia.Base/Data/BindingValue.cs
  34. 2
      src/Avalonia.Base/Data/Core/StreamNode.cs
  35. 4
      src/Avalonia.Base/Diagnostics/IAvaloniaObjectDebug.cs
  36. 9
      src/Avalonia.Base/IAvaloniaObject.cs
  37. 81
      src/Avalonia.Base/Metadata/NullableAttributes.cs
  38. 2
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  39. 9
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  40. 2
      src/Avalonia.Controls.DataGrid/DataGridRows.cs
  41. 4
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  42. 4
      src/Avalonia.Controls/ApiCompatBaseline.txt
  43. 31
      src/Avalonia.Controls/Application.cs
  44. 2
      src/Avalonia.Controls/ComboBox.cs
  45. 2
      src/Avalonia.Controls/Control.cs
  46. 44
      src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs
  47. 32
      src/Avalonia.Controls/Converters/CornerRadiusFilterKind.cs
  48. 4
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  49. 6
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  50. 42
      src/Avalonia.Controls/Expander.cs
  51. 3
      src/Avalonia.Controls/ExperimentalAcrylicBorder.cs
  52. 32
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  53. 3
      src/Avalonia.Controls/Grid.cs
  54. 6
      src/Avalonia.Controls/GridLength.cs
  55. 4
      src/Avalonia.Controls/IControl.cs
  56. 6
      src/Avalonia.Controls/Image.cs
  57. 1
      src/Avalonia.Controls/Menu.cs
  58. 6
      src/Avalonia.Controls/MenuItem.cs
  59. 2
      src/Avalonia.Controls/Presenters/CarouselPresenter.cs
  60. 2
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  61. 22
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  62. 22
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  63. 3
      src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs
  64. 18
      src/Avalonia.Controls/Primitives/ScrollBarVisibility.cs
  65. 3
      src/Avalonia.Controls/RadioButton.cs
  66. 3
      src/Avalonia.Controls/RelativePanel.cs
  67. 2
      src/Avalonia.Controls/Remote/RemoteWidget.cs
  68. 3
      src/Avalonia.Controls/RepeatButton.cs
  69. 1
      src/Avalonia.Controls/Repeater/ViewportManager.cs
  70. 1
      src/Avalonia.Controls/Repeater/VirtualizationInfo.cs
  71. 8
      src/Avalonia.Controls/RowDefinitions.cs
  72. 36
      src/Avalonia.Controls/ScrollViewer.cs
  73. 26
      src/Avalonia.Controls/Shapes/Shape.cs
  74. 2
      src/Avalonia.Controls/SplitView.cs
  75. 129
      src/Avalonia.Controls/TextBox.cs
  76. 3
      src/Avalonia.Controls/TickBar.cs
  77. 5
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  78. 11
      src/Avalonia.Controls/Utils/UndoRedoHelper.cs
  79. 87
      src/Avalonia.Controls/Viewbox.cs
  80. 12
      src/Avalonia.Controls/Window.cs
  81. 4
      src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
  82. 103
      src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml
  83. 52
      src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs
  84. 87
      src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml
  85. 33
      src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs
  86. 22
      src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs
  87. 11
      src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs
  88. 25
      src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs
  89. 14
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  90. 9
      src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
  91. 4
      src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs
  92. 6
      src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
  93. 4
      src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs
  94. 49
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
  95. 19
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
  96. 3
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs
  97. 68
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  98. 140
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs
  99. 12
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs
  100. 30
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs

2
.github/PULL_REQUEST_TEMPLATE.md

@ -18,7 +18,7 @@
- [ ] Added unit tests (if possible)?
- [ ] Added XML documentation to any related classes?
- [ ] Consider submitting a PR to https://github.com/AvaloniaUI/Avaloniaui.net with user documentation
- [ ] Consider submitting a PR to https://github.com/AvaloniaUI/Documentation with user documentation
## Breaking changes
<!--- List any breaking changes here. When the PR is merged please add an entry to https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes -->

22
Documentation/build.md

@ -9,10 +9,24 @@ git clone https://github.com/AvaloniaUI/Avalonia.git
git submodule update --init
```
### Install the required version of the .NET Core SDK
Go to https://dotnet.microsoft.com/download/visual-studio-sdks and install the latest version of the .NET Core SDK compatible with Avalonia UI. Make sure to download the SDK (not just the "runtime") package. The version compatible is indicated within the [global.json](https://github.com/AvaloniaUI/Avalonia/blob/master/global.json) file. Note that Avalonia UI does not always use the latest version and is hardcoded to use the last version known to be compatible (SDK releases may break the builds from time-to-time).
### Open in Visual Studio
Open the `Avalonia.sln` solution in Visual Studio 2019 or newer. The free Visual Studio Community
edition works fine. Run the `Samples\ControlCatalog.Desktop` project to see the sample application.
Open the `Avalonia.sln` solution in Visual Studio 2019 or newer. The free Visual Studio Community edition works fine. Build and run the `Samples\ControlCatalog.Desktop` or `ControlCatalog.NetCore` project to see the sample application.
### Troubleshooting
* **Error CS0006: Avalonia.DesktopRuntime.dll could not be found**
It is common for the first build to fail with the errors below (also discussed in [#4257](https://github.com/AvaloniaUI/Avalonia/issues/4257)).
```
>CSC : error CS0006: Metadata file 'C:\...\Avalonia\src\Avalonia.DesktopRuntime\bin\Debug\netcoreapp2.0\Avalonia.DesktopRuntime.dll' could not be found
>CSC : error CS0006: Metadata file 'C:\...\Avalonia\packages\Avalonia\bin\Debug\netcoreapp2.0\Avalonia.dll' could not be found
```
To correct this, right click on the `Avalonia.DesktopRuntime` project then press `Build` to build the project manually. Afterwards the solution should build normally and the ControlCatalog can be run.
# Linux/macOS
@ -20,9 +34,9 @@ It's *not* possible to build the *whole* project on Linux/macOS. You can only bu
MonoDevelop, Xamarin Studio and Visual Studio for Mac aren't capable of properly opening our solution. You can use Rider (at least 2017.2 EAP) or VSCode instead. They will fail to load most of platform specific projects, but you don't need them to run on .NET Core.
### Install the latest version of .NET Core
### Install the latest version of the .NET Core SDK
Go to https://www.microsoft.com/net/core and follow instructions for your OS. You need SDK (not just "runtime") package.
Go to https://www.microsoft.com/net/core and follow the instructions for your OS. Make sure to download the SDK (not just the "runtime") package.
### Additional requirements for macOS

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

@ -50,7 +50,6 @@ public:
[Window setBackingType:NSBackingStoreBuffered];
[Window setOpaque:false];
[Window setContentView: StandardContainer];
}
virtual HRESULT ObtainNSWindowHandle(void** ret) override
@ -112,6 +111,9 @@ public:
{
SetPosition(lastPositionSet);
UpdateStyle();
[Window setContentView: StandardContainer];
if(ShouldTakeFocusOnShow() && activate)
{
[Window makeKeyAndOrderFront:Window];
@ -124,7 +126,7 @@ public:
[Window setTitle:_lastTitle];
_shown = true;
return S_OK;
}
}
@ -191,9 +193,11 @@ public:
{
if(ret == nullptr)
return E_POINTER;
auto frame = [View frame];
ret->Width = frame.size.width;
ret->Height = frame.size.height;
return S_OK;
}
}
@ -254,6 +258,12 @@ public:
y = maxSize.height;
}
if(!_shown)
{
BaseEvents->Resized(AvnSize{x,y});
}
[StandardContainer setFrameSize:NSSize{x,y}];
[Window setContentSize:NSSize{x, y}];
return S_OK;
@ -503,6 +513,7 @@ private:
bool _fullScreenActive;
SystemDecorations _decorations;
AvnWindowState _lastWindowState;
AvnWindowState _actualWindowState;
bool _inSetWindowState;
NSRect _preZoomSize;
bool _transitioningWindowState;
@ -529,6 +540,7 @@ private:
_transitioningWindowState = false;
_inSetWindowState = false;
_lastWindowState = Normal;
_actualWindowState = Normal;
WindowEvents = events;
[Window setCanBecomeKeyAndMain];
[Window disableCursorRects];
@ -623,7 +635,7 @@ private:
void WindowStateChanged () override
{
if(!_inSetWindowState && !_transitioningWindowState)
if(_shown && !_inSetWindowState && !_transitioningWindowState)
{
AvnWindowState state;
GetWindowState(&state);
@ -953,14 +965,14 @@ private:
{
@autoreleasepool
{
if(_lastWindowState == state)
if(_actualWindowState == state)
{
return S_OK;
}
_inSetWindowState = true;
auto currentState = _lastWindowState;
auto currentState = _actualWindowState;
_lastWindowState = state;
if(currentState == Normal)
@ -1039,8 +1051,11 @@ private:
}
break;
}
_actualWindowState = _lastWindowState;
}
_inSetWindowState = false;
return S_OK;
@ -1996,7 +2011,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_lastScaling = [self backingScaleFactor];
[self setOpaque:NO];
[self setBackgroundColor: [NSColor clearColor]];
[self invalidateShadow];
_isExtended = false;
return self;
}
@ -2237,6 +2251,7 @@ protected:
{
if (Window != nullptr)
{
[StandardContainer setFrameSize:NSSize{x,y}];
[Window setContentSize:NSSize{x, y}];
[Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))];

4
readme.md

@ -14,7 +14,7 @@ To see the status of some of our features, please see our [Roadmap](https://gith
## 🚀 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 starter guide see our [documentation](https://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 starter guide see our [documentation](https://docs.avaloniaui.net/docs/getting-started).
Avalonia is delivered via <b>NuGet</b> package manager. You can find the packages here: https://www.nuget.org/packages/Avalonia/
@ -52,7 +52,7 @@ We also have a [nightly build](https://github.com/AvaloniaUI/Avalonia/wiki/Using
## Documentation
Documentation can be found on our website at https://avaloniaui.net/docs/. We also have a [tutorial](https://avaloniaui.net/docs/tutorial/) over there for newcomers.
Documentation can be found at https://docs.avaloniaui.net. We also have a [tutorial](https://docs.avaloniaui.net/docs/getting-started/programming-with-avalonia) over there for newcomers.
## Building and Using

8
samples/ControlCatalog/App.xaml.cs

@ -39,6 +39,10 @@ namespace ControlCatalog
public static Styles DefaultLight = new Styles
{
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml")
},
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml")
@ -60,6 +64,10 @@ namespace ControlCatalog
public static Styles DefaultDark = new Styles
{
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml")
},
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml")

5
samples/ControlCatalog/MainWindow.xaml.cs

@ -17,7 +17,10 @@ namespace ControlCatalog
public MainWindow()
{
this.InitializeComponent();
this.AttachDevTools();
this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()
{
StartupScreenIndex = 1,
});
//Renderer.DrawFps = true;
//Renderer.DrawDirtyRects = Renderer.DrawFps = true;

6
samples/ControlCatalog/Pages/ContextFlyoutPage.axaml

@ -84,13 +84,13 @@
<Button Name="CutButton" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}">
<PathIcon Width="14" Height="14" Data="M5.22774,2.08072 C5.43359778,1.94704 5.7011484,1.98419259 5.86368634,2.15675215 L5.91939,2.22774 L12.5191,12.3904 C12.956,12.1419 13.4614,12.0000019 14,12.0000019 C15.6569,12.0000019 17,13.3431 17,15.0000019 C17,16.6569 15.6569,18.0000019 14,18.0000019 C12.3431,18.0000019 11,16.6569 11,15.0000019 C11,14.3201402 11.226152,13.693011 11.6073785,13.1899092 L11.7401,13.0269 L10,10.3474 L8.25991,13.0269 C8.72078,13.5543 9,14.2446 9,15.0000019 C9,16.6569 7.65685,18.0000019 6,18.0000019 C4.34315,18.0000019 3,16.6569 3,15.0000019 C3,13.3431 4.34315,12.0000019 6,12.0000019 C6.46163143,12.0000019 6.89890041,12.1042536 7.28955831,12.2905296 L7.4809,12.3904 L9.40382,9.42936 L5.08072,2.77238 C4.93033,2.54079 4.99615,2.23112 5.22774,2.08072 Z M14,13 C12.8954,13 12,13.8954 12,15 C12,16.1046 12.8954,17 14,17 C15.1046,17 16,16.1046 16,15 C16,13.8954 15.1046,13 14,13 Z M6,13 C4.89543,13 4,13.8954 4,15 C4,16.1046 4.89543,17 6,17 C7.10457,17 8,16.1046 8,15 C8,13.8954 7.10457,13 6,13 Z M14.7723,2.08072 C15.0039,2.23112 15.0697,2.54079 14.9193,2.77238 L11.1924,8.51133 L10.5962,7.59329 L14.0806,2.22774 C14.231,1.99615 14.5407,1.93033 14.7723,2.08072 Z" />
</Button>
<Button Name="CopyButton" Content="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}">
<Button Name="CopyButton" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}">
<PathIcon Width="14" Height="14" Data="M5.50280381,4.62704038 L5.5,6.75 L5.5,17.2542087 C5.5,19.0491342 6.95507456,20.5042087 8.75,20.5042087 L17.3662868,20.5044622 C17.057338,21.3782241 16.2239751,22.0042087 15.2444057,22.0042087 L8.75,22.0042087 C6.12664744,22.0042087 4,19.8775613 4,17.2542087 L4,6.75 C4,5.76928848 4.62744523,4.93512464 5.50280381,4.62704038 Z M17.75,2 C18.9926407,2 20,3.00735931 20,4.25 L20,17.25 C20,18.4926407 18.9926407,19.5 17.75,19.5 L8.75,19.5 C7.50735931,19.5 6.5,18.4926407 6.5,17.25 L6.5,4.25 C6.5,3.00735931 7.50735931,2 8.75,2 L17.75,2 Z M17.75,3.5 L8.75,3.5 C8.33578644,3.5 8,3.83578644 8,4.25 L8,17.25 C8,17.6642136 8.33578644,18 8.75,18 L17.75,18 C18.1642136,18 18.5,17.6642136 18.5,17.25 L18.5,4.25 C18.5,3.83578644 18.1642136,3.5 17.75,3.5 Z" />
</Button>
<Button Name="PasteButton" Content="Paste" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}">
<Button Name="PasteButton" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}">
<PathIcon Width="14" Height="14" Data="M13.75,2 C14.940864,2 15.9156449,2.92516159 15.9948092,4.09595119 L16,4.25 L16,4.25 C16,4.16530567 15.9953205,4.0817043 15.9862059,3.99944035 L17.75,4 C18.9926407,4 20,5.00735931 20,6.25 L20,19.75 C20,20.9926407 18.9926407,22 17.75,22 L6.25,22 C5.00735931,22 4,20.9926407 4,19.75 L4,6.25 C4,5.00735931 5.00735931,4 6.25,4 L8.01379413,3.99944035 C8.00733496,4.05773764 8.00310309,4.11670658 8.00118552,4.17626017 L8,4.25 C8,3.00735931 9.00735931,2 10.25,2 L13.75,2 Z M13.75,6.5 L10.25,6.5 C9.45594921,6.5 8.75796956,6.08867052 8.357512,5.4674625 L8.37902077,5.50019943 L8.37902077,5.50019943 L6.25,5.5 C5.83578644,5.5 5.5,5.83578644 5.5,6.25 L5.5,19.75 C5.5,20.1642136 5.83578644,20.5 6.25,20.5 L17.75,20.5 C18.1642136,20.5 18.5,20.1642136 18.5,19.75 L18.5,6.25 C18.5,5.83578644 18.1642136,5.5 17.75,5.5 L15.6209792,5.50019943 L15.642488,5.4674625 C15.2420304,6.08867052 14.5440508,6.5 13.75,6.5 Z M13.75,3.5 L10.25,3.5 C9.83578644,3.5 9.5,3.83578644 9.5,4.25 C9.5,4.66421356 9.83578644,5 10.25,5 L13.75,5 C14.1642136,5 14.5,4.66421356 14.5,4.25 C14.5,3.83578644 14.1642136,3.5 13.75,3.5 Z" />
</Button>
<Button Name="ClearButton" Content="Clear" Command="{Binding $parent[TextBox].Clear}">
<Button Name="ClearButton" Command="{Binding $parent[TextBox].Clear}">
<PathIcon Width="14" Height="14" Data="M3.52499419,3.71761187 L3.61611652,3.61611652 C4.0717282,3.16050485 4.79154862,3.13013074 5.28238813,3.52499419 L5.38388348,3.61611652 L14,12.233 L22.6161165,3.61611652 C23.1042719,3.12796116 23.8957281,3.12796116 24.3838835,3.61611652 C24.8720388,4.10427189 24.8720388,4.89572811 24.3838835,5.38388348 L15.767,14 L24.3838835,22.6161165 C24.8394952,23.0717282 24.8698693,23.7915486 24.4750058,24.2823881 L24.3838835,24.3838835 C23.9282718,24.8394952 23.2084514,24.8698693 22.7176119,24.4750058 L22.6161165,24.3838835 L14,15.767 L5.38388348,24.3838835 C4.89572811,24.8720388 4.10427189,24.8720388 3.61611652,24.3838835 C3.12796116,23.8957281 3.12796116,23.1042719 3.61611652,22.6161165 L12.233,14 L3.61611652,5.38388348 C3.16050485,4.9282718 3.13013074,4.20845138 3.52499419,3.71761187 L3.61611652,3.61611652 L3.52499419,3.71761187 Z" />
</Button>
</StackPanel>

76
samples/ControlCatalog/Pages/ViewboxPage.xaml

@ -1,66 +1,36 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ViewboxPage">
<UserControl.Resources>
<StreamGeometry x:Key="Acorn">
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
</StreamGeometry>
</UserControl.Resources>
<Grid RowDefinitions="Auto,*">
<Grid RowDefinitions="Auto,*,*">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Viewbox</TextBlock>
<TextBlock Classes="h2">A control used to scale single child.</TextBlock>
</StackPanel>
<Grid ColumnDefinitions="Auto,*,*"
RowDefinitions="*,*,*,*"
Grid.Row="1" Margin="48"
MaxWidth="400">
<TextBlock Grid.Row="0" VerticalAlignment="Center">None</TextBlock>
<TextBlock Grid.Row="1" VerticalAlignment="Center">Fill</TextBlock>
<TextBlock Grid.Row="2" VerticalAlignment="Center">Uniform</TextBlock>
<TextBlock Grid.Row="3" VerticalAlignment="Center">UniformToFill</TextBlock>
<Viewbox Grid.Row="0" Grid.Column="1" Stretch="None">
<TextBlock>Hello World!</TextBlock>
</Viewbox>
<Viewbox Grid.Row="1" Grid.Column="1" Stretch="Fill">
<TextBlock>Hello World!</TextBlock>
</Viewbox>
<Viewbox Grid.Row="2" Grid.Column="1" Stretch="Uniform">
<TextBlock>Hello World!</TextBlock>
</Viewbox>
<Viewbox Grid.Row="3" Grid.Column="1" Stretch="UniformToFill">
<TextBlock>Hello World!</TextBlock>
</Viewbox>
<Grid Grid.Row="1" ColumnDefinitions="*,Auto" HorizontalAlignment="Center" Margin="0,10,0,0">
<Viewbox Grid.Row="0" Grid.Column="2" Stretch="None">
<Path Fill="Blue" Data="{StaticResource Acorn}"/>
</Viewbox>
<Viewbox Grid.Row="1" Grid.Column="2" Stretch="Fill">
<Path Fill="Blue" Data="{StaticResource Acorn}"/>
</Viewbox>
<Viewbox Grid.Row="2" Grid.Column="2" Stretch="Uniform">
<Path Fill="Blue" Data="{StaticResource Acorn}"/>
</Viewbox>
<Viewbox Grid.Row="3" Grid.Column="2" Stretch="UniformToFill">
<Path Fill="Blue" Data="{StaticResource Acorn}"/>
</Viewbox>
<Border HorizontalAlignment="Center" Grid.Column="0" BorderThickness="1" BorderBrush="Orange" Width="200" Height="200">
<Border VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Column="0" BorderThickness="1" BorderBrush="CornflowerBlue" Width="{Binding #WidthSlider.Value}" Height="{Binding #HeightSlider.Value}" >
<Viewbox
Stretch="{Binding #StretchSelector.SelectedItem}"
StretchDirection="{Binding #StretchDirectionSelector.SelectedItem}">
<Ellipse Width="50" Height="50" Fill="CornflowerBlue" />
</Viewbox>
</Border>
</Border>
<StackPanel HorizontalAlignment="Left" Orientation="Vertical" Grid.Column="1" Margin="8,0,0,0" Width="150">
<TextBlock Text="Width" />
<Slider Minimum="10" Maximum="200" Value="100" x:Name="WidthSlider" TickFrequency="25" TickPlacement="TopLeft" />
<TextBlock Text="Height" />
<Slider Minimum="10" Maximum="200" Value="100" x:Name="HeightSlider" TickFrequency="25" TickPlacement="TopLeft" />
<TextBlock Text="Stretch" />
<ComboBox x:Name="StretchSelector" HorizontalAlignment="Stretch" Margin="0,0,0,2" />
<TextBlock Text="Stretch Direction" />
<ComboBox x:Name="StretchDirectionSelector" HorizontalAlignment="Stretch" />
</StackPanel>
</Grid>
</Grid>
</UserControl>

21
samples/ControlCatalog/Pages/ViewboxPage.xaml.cs

@ -1,5 +1,6 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
namespace ControlCatalog.Pages
{
@ -7,7 +8,25 @@ namespace ControlCatalog.Pages
{
public ViewboxPage()
{
this.InitializeComponent();
InitializeComponent();
var stretchSelector = this.FindControl<ComboBox>("StretchSelector");
stretchSelector.Items = new[]
{
Stretch.Uniform, Stretch.UniformToFill, Stretch.Fill, Stretch.None
};
stretchSelector.SelectedIndex = 0;
var stretchDirectionSelector = this.FindControl<ComboBox>("StretchDirectionSelector");
stretchDirectionSelector.Items = new[]
{
StretchDirection.Both, StretchDirection.DownOnly, StretchDirection.UpOnly
};
stretchDirectionSelector.SelectedIndex = 0;
}
private void InitializeComponent()

3
samples/RenderDemo/MainWindow.xaml

@ -36,6 +36,9 @@
<TabItem Header="Transitions">
<pages:TransitionsPage/>
</TabItem>
<TabItem Header="Custom Animator">
<pages:CustomAnimatorPage/>
</TabItem>
<TabItem Header="Clipping">
<pages:ClippingPage/>
</TabItem>

7
samples/RenderDemo/Pages/AnimationsPage.xaml

@ -1,7 +1,8 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="RenderDemo.Pages.AnimationsPage">
x:Class="RenderDemo.Pages.AnimationsPage"
MaxWidth="600">
<UserControl.Styles>
<Styles>
<Styles.Resources>
@ -167,8 +168,8 @@
<StackPanel.Clock>
<Clock />
</StackPanel.Clock>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center">Hover to activate Transform Keyframe Animations.</TextBlock>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="20">
<TextBlock VerticalAlignment="Center">Hover to activate Keyframe Animations.</TextBlock>
<Button Content="{Binding PlayStateText}" Command="{Binding TogglePlayState}" Click="ToggleClock" />
</StackPanel>
<WrapPanel ClipToBounds="False">

25
samples/RenderDemo/Pages/CustomAnimatorPage.xaml

@ -0,0 +1,25 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:pages="clr-namespace:RenderDemo.Pages"
x:Class="RenderDemo.Pages.CustomAnimatorPage"
MaxWidth="600">
<Grid>
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock.Styles>
<Style Selector="TextBlock">
<Style.Animations>
<Animation Duration="0:0:1" IterationCount="Infinite">
<KeyFrame Cue="0%">
<Setter Property="Text" Value="" Animation.Animator="{x:Type pages:CustomStringAnimator}"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Text" Value="0123456789" Animation.Animator="{x:Type pages:CustomStringAnimator}"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</TextBlock.Styles>
</TextBlock>
</Grid>
</UserControl>

27
samples/RenderDemo/Pages/CustomAnimatorPage.xaml.cs

@ -0,0 +1,27 @@
using System.Reactive.Linq;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using RenderDemo.ViewModels;
namespace RenderDemo.Pages
{
public class CustomAnimatorPage : UserControl
{
public CustomAnimatorPage()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

16
samples/RenderDemo/Pages/CustomStringAnimator.cs

@ -0,0 +1,16 @@
using Avalonia.Animation.Animators;
namespace RenderDemo.Pages
{
public class CustomStringAnimator : Animator<string>
{
public override string Interpolate(double progress, string oldValue, string newValue)
{
if (newValue.Length == 0) return "";
var step = 1.0 / newValue.Length;
var length = (int)(progress / step);
var result = newValue.Substring(0, length + 1);
return result;
}
}
}

101
samples/RenderDemo/Pages/TransitionsPage.xaml

@ -1,7 +1,8 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="RenderDemo.Pages.TransitionsPage">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="RenderDemo.Pages.TransitionsPage"
MaxWidth="600">
<UserControl.Styles>
<Styles>
<Styles.Resources>
@ -90,6 +91,89 @@
<Setter Property="RenderTransform" Value="none" />
</Style>
<Style Selector="Border.Rect7">
<Setter Property="Transitions">
<Transitions>
<DoubleTransition Property="Height" Duration="0:0:0.5" />
</Transitions>
</Setter>
</Style>
<Style Selector="Border.Rect7:pointerover">
<Setter Property="Height" Value="50" />
</Style>
<Style Selector="Border.Rect8">
<Setter Property="Transitions">
<Transitions>
<CornerRadiusTransition Property="CornerRadius" Duration="0:0:0.5" />
</Transitions>
</Setter>
</Style>
<Style Selector="Border.Rect8:pointerover">
<Setter Property="CornerRadius" Value="50" />
</Style>
<Style Selector="Border.Rect9">
<Setter Property="Transitions">
<Transitions>
<ThicknessTransition Property="Padding" Duration="0:0:0.5" />
</Transitions>
</Setter>
</Style>
<Style Selector="Border.Rect9:pointerover">
<Setter Property="Padding" Value="10" />
</Style>
<Style Selector="Border.Shadow">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BoxShadow" Value="inset 0 0 0 2 Red, -15 -15 Green"/>
<Setter Property="Transitions">
<Transitions>
<BoxShadowsTransition Property="BoxShadow" Duration="0:0:0.5" />
</Transitions>
</Setter>
</Style>
<Style Selector="Border.Shadow:pointerover">
<Setter Property="BoxShadow" Value="inset 30 30 20 30 Green, 20 40 20 10 Red"/>
</Style>
<Style Selector="Border.Rect10">
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Background" Duration="0:0:0.5" />
</Transitions>
</Setter>
<Setter Property="Background" Value="Red" />
</Style>
<Style Selector="Border.Rect10:pointerover">
<Setter Property="Background" Value="Orange" />
</Style>
<Style Selector="Border.Rect11">
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Background" Duration="0:0:0.5" />
</Transitions>
</Setter>
<Setter Property="Background" Value="Red" />
</Style>
<Style Selector="Border.Rect11:pointerover">
<Setter Property="Background" >
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0" Color="Red"/>
<GradientStop Offset="1" Color="Blue"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Setter>
</Style>
</Styles>
</UserControl.Styles>
@ -98,8 +182,8 @@
<StackPanel.Clock>
<Clock />
</StackPanel.Clock>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center">Hover to activate Transform Keyframe Animations.</TextBlock>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="20">
<TextBlock VerticalAlignment="Center">Hover to activate Transitions.</TextBlock>
<Button Content="{Binding PlayStateText}" Command="{Binding TogglePlayState}" Click="ToggleClock" />
</StackPanel>
<WrapPanel ClipToBounds="False">
@ -109,6 +193,15 @@
<Border Classes="Test Rect4" Background="Navy"/>
<Border Classes="Test Rect5" Background="SeaGreen"/>
<Border Classes="Test Rect6" Background="Orange"/>
<Border Classes="Test Rect7" Background="Gold"/>
<Border Classes="Test Rect8" Background="Gray" />
<Border Classes="Test Rect9" Background="Red" />
<Border Classes="Test Shadow" CornerRadius="10" Child="{x:Null}" />
<Border Classes="Test Shadow" CornerRadius="0 30 60 0" Child="{x:Null}" />
<Border Classes="Test Rect10" />
<Border Classes="Test Rect11" />
</WrapPanel>
</StackPanel>
</Grid>

70
src/Avalonia.Animation/Animation.cs

@ -3,10 +3,11 @@ using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation.Animators;
using Avalonia.Animation.Easings;
using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Metadata;
@ -194,6 +195,33 @@ namespace Avalonia.Animation
[Content]
public KeyFrames Children { get; } = new KeyFrames();
// Store values for the Animator attached properties for IAnimationSetter objects.
private static readonly Dictionary<IAnimationSetter, Type> s_animators = new Dictionary<IAnimationSetter, Type>();
/// <summary>
/// Gets the value of the Animator attached property for a setter.
/// </summary>
/// <param name="setter">The animation setter.</param>
/// <returns>The property animator type.</returns>
public static Type GetAnimator(IAnimationSetter setter)
{
if (s_animators.TryGetValue(setter, out var type))
{
return type;
}
return null;
}
/// <summary>
/// Sets the value of the Animator attached property for a setter.
/// </summary>
/// <param name="setter">The animation setter.</param>
/// <param name="value">The property animator value.</param>
public static void SetAnimator(IAnimationSetter setter, Type value)
{
s_animators[setter] = value;
}
private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator)> Animators = new List<(Func<AvaloniaProperty, bool>, Type)>
{
( prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator) ),
@ -209,6 +237,17 @@ namespace Avalonia.Animation
( prop => typeof(decimal).IsAssignableFrom(prop.PropertyType), typeof(DecimalAnimator) ),
};
/// <summary>
/// Registers a <see cref="Animator{T}"/> that can handle
/// a value type that matches the specified condition.
/// </summary>
/// <param name="condition">
/// The condition to which the <see cref="Animator{T}"/>
/// is to be activated and used.
/// </param>
/// <typeparam name="TAnimator">
/// The type of the animator to instantiate.
/// </typeparam>
public static void RegisterAnimator<TAnimator>(Func<AvaloniaProperty, bool> condition)
where TAnimator : IAnimator
{
@ -237,7 +276,7 @@ namespace Avalonia.Animation
{
foreach (var setter in keyframe.Setters)
{
var handler = GetAnimatorType(setter.Property);
var handler = Animation.GetAnimator(setter) ?? GetAnimatorType(setter.Property);
if (handler == null)
{
@ -281,7 +320,7 @@ namespace Avalonia.Animation
return (newAnimatorInstances, subscriptions);
}
/// <inheritdocs/>
/// <inheritdoc/>
public IDisposable Apply(Animatable control, IClock clock, IObservable<bool> match, Action onComplete)
{
var (animators, subscriptions) = InterpretKeyframes(control);
@ -306,25 +345,40 @@ namespace Avalonia.Animation
if (onComplete != null)
{
Task.WhenAll(completionTasks).ContinueWith(_ => onComplete());
Task.WhenAll(completionTasks).ContinueWith(
(_, state) => ((Action)state).Invoke(),
onComplete);
}
}
return new CompositeDisposable(subscriptions);
}
/// <inheritdocs/>
public Task RunAsync(Animatable control, IClock clock = null)
/// <inheritdoc/>
public Task RunAsync(Animatable control, IClock clock = null, CancellationToken cancellationToken = default)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.CompletedTask;
}
var run = new TaskCompletionSource<object>();
if (this.IterationCount == IterationCount.Infinite)
run.SetException(new InvalidOperationException("Looping animations must not use the Run method."));
IDisposable subscriptions = null;
IDisposable subscriptions = null, cancellation = null;
subscriptions = this.Apply(control, clock, Observable.Return(true), () =>
{
run.SetResult(null);
run.TrySetResult(null);
subscriptions?.Dispose();
cancellation?.Dispose();
});
cancellation = cancellationToken.Register(() =>
{
run.TrySetResult(null);
subscriptions?.Dispose();
cancellation?.Dispose();
});
return run.Task;

25
src/Avalonia.Animation/AnimationInstance`1.cs

@ -5,6 +5,7 @@ using Avalonia.Animation.Animators;
using Avalonia.Animation.Utils;
using Avalonia.Data;
using Avalonia.Reactive;
using JetBrains.Annotations;
namespace Avalonia.Animation
{
@ -36,6 +37,7 @@ namespace Avalonia.Animation
private IDisposable _timerSub;
private readonly IClock _baseClock;
private IClock _clock;
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChangedDelegate;
public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, IClock baseClock, Action OnComplete, Func<double, T, T> Interpolator)
{
@ -45,8 +47,6 @@ namespace Avalonia.Animation
_onCompleteAction = OnComplete;
_interpolator = Interpolator;
_baseClock = baseClock;
_neutralValue = (T)_targetControl.GetValue(_animator.Property);
FetchProperties();
}
@ -80,6 +80,7 @@ namespace Avalonia.Animation
// Animation may have been stopped before it has finished.
ApplyFinalFill();
_targetControl.PropertyChanged -= _propertyChangedDelegate;
_timerSub?.Dispose();
_clock.PlayState = PlayState.Stop;
}
@ -88,6 +89,9 @@ namespace Avalonia.Animation
{
_clock = new Clock(_baseClock);
_timerSub = _clock.Subscribe(Step);
_propertyChangedDelegate ??= ControlPropertyChanged;
_targetControl.PropertyChanged += _propertyChangedDelegate;
UpdateNeutralValue();
}
public void Step(TimeSpan frameTick)
@ -216,5 +220,22 @@ namespace Avalonia.Animation
}
}
}
private void UpdateNeutralValue()
{
var property = _animator.Property;
var baseValue = _targetControl.GetBaseValue(property, BindingPriority.LocalValue);
_neutralValue = baseValue != AvaloniaProperty.UnsetValue ?
(T)baseValue : (T)_targetControl.GetValue(property);
}
private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == _animator.Property && e.Priority > BindingPriority.Animation)
{
UpdateNeutralValue();
}
}
}
}

20
src/Avalonia.Animation/AnimatorDrivenTransition.cs

@ -0,0 +1,20 @@
using System;
using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
/// <summary>
/// <see cref="Transition{T}"/> using an <see cref="Animator{T}"/> to transition between values.
/// </summary>
/// <typeparam name="T">Type of the transitioned value.</typeparam>
/// <typeparam name="TAnimator">Type of the animator.</typeparam>
public abstract class AnimatorDrivenTransition<T, TAnimator> : Transition<T> where TAnimator : Animator<T>, new()
{
private static readonly TAnimator s_animator = new TAnimator();
public override IObservable<T> DoTransition(IObservable<double> progress, T oldValue, T newValue)
{
return new AnimatorTransitionObservable<T, TAnimator>(s_animator, progress, Easing, oldValue, newValue);
}
}
}

32
src/Avalonia.Animation/AnimatorTransitionObservable.cs

@ -0,0 +1,32 @@
using System;
using Avalonia.Animation.Animators;
using Avalonia.Animation.Easings;
namespace Avalonia.Animation
{
/// <summary>
/// Transition observable based on an <see cref="Animator{T}"/> producing a value.
/// </summary>
/// <typeparam name="T">Type of the transitioned value.</typeparam>
/// <typeparam name="TAnimator">Type of the animator.</typeparam>
public class AnimatorTransitionObservable<T, TAnimator> : TransitionObservableBase<T> where TAnimator : Animator<T>
{
private readonly TAnimator _animator;
private readonly Easing _easing;
private readonly T _oldValue;
private readonly T _newValue;
public AnimatorTransitionObservable(TAnimator animator, IObservable<double> progress, Easing easing, T oldValue, T newValue) : base(progress, easing)
{
_animator = animator;
_easing = easing;
_oldValue = oldValue;
_newValue = newValue;
}
protected override T ProduceValue(double progress)
{
return _animator.Interpolate(progress, _oldValue, _newValue);
}
}
}

8
src/Avalonia.Animation/Animators/Animator`1.cs

@ -104,6 +104,11 @@ namespace Avalonia.Animation.Animators
throw new Exception("Index time is out of keyframe time range.");
}
public virtual IDisposable BindAnimation(Animatable control, IObservable<T> instance)
{
return control.Bind((AvaloniaProperty<T>)Property, instance, BindingPriority.Animation);
}
/// <summary>
/// Runs the KeyFrames Animation.
/// </summary>
@ -116,7 +121,8 @@ namespace Avalonia.Animation.Animators
clock ?? control.Clock ?? Clock.GlobalClock,
onComplete,
InterpolationHandler);
return control.Bind<T>((AvaloniaProperty<T>)Property, instance, BindingPriority.Animation);
return BindAnimation(control, instance);
}
/// <summary>

6
src/Avalonia.Animation/ApiCompatBaseline.txt

@ -0,0 +1,6 @@
Compat issues with assembly Avalonia.Animation:
MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.Animation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock, System.Threading.CancellationToken)' is present in the implementation but not in the contract.
Total Issues: 4

9
src/Avalonia.Animation/Clock.cs

@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Text;
using Avalonia.Reactive;
namespace Avalonia.Animation
{
@ -10,10 +6,9 @@ namespace Avalonia.Animation
{
public static IClock GlobalClock => AvaloniaLocator.Current.GetService<IGlobalClock>();
private IDisposable _parentSubscription;
private readonly IDisposable _parentSubscription;
public Clock()
:this(GlobalClock)
public Clock() : this(GlobalClock)
{
}

12
src/Avalonia.Animation/ClockBase.cs

@ -1,16 +1,11 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Text;
using Avalonia.Reactive;
namespace Avalonia.Animation
{
public class ClockBase : IClock
{
private ClockObservable _observable;
private IObservable<TimeSpan> _connectedObservable;
private readonly ClockObservable _observable;
private TimeSpan? _previousTime;
private TimeSpan _internalTime;
@ -18,7 +13,6 @@ namespace Avalonia.Animation
protected ClockBase()
{
_observable = new ClockObservable();
_connectedObservable = _observable.Publish().RefCount();
}
protected bool HasSubscriptions => _observable.HasSubscriptions;
@ -58,10 +52,10 @@ namespace Avalonia.Animation
public IDisposable Subscribe(IObserver<TimeSpan> observer)
{
return _connectedObservable.Subscribe(observer);
return _observable.Subscribe(observer);
}
private class ClockObservable : LightweightObservableBase<TimeSpan>
private sealed class ClockObservable : LightweightObservableBase<TimeSpan>
{
public bool HasSubscriptions { get; private set; }
public void Pulse(TimeSpan time) => PublishNext(time);

3
src/Avalonia.Animation/IAnimation.cs

@ -1,4 +1,5 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Avalonia.Animation
@ -16,6 +17,6 @@ namespace Avalonia.Animation
/// <summary>
/// Run the animation on the specified control.
/// </summary>
Task RunAsync(Animatable control, IClock clock);
Task RunAsync(Animatable control, IClock clock, CancellationToken cancellationToken = default);
}
}

6
src/Avalonia.Animation/Transition`1.cs → src/Avalonia.Animation/Transition.cs

@ -1,7 +1,5 @@
using System;
using System.Reactive.Linq;
using System;
using Avalonia.Animation.Easings;
using Avalonia.Animation.Utils;
namespace Avalonia.Animation
{
@ -56,4 +54,4 @@ namespace Avalonia.Animation
return control.Bind<T>((AvaloniaProperty<T>)Property, transition, Data.BindingPriority.Animation);
}
}
}
}

60
src/Avalonia.Animation/TransitionInstance.cs

@ -1,8 +1,5 @@
using Avalonia.Metadata;
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Easings;
using Avalonia.Animation.Utils;
using System.Runtime.ExceptionServices;
using Avalonia.Reactive;
using Avalonia.Utilities;
@ -11,13 +8,13 @@ namespace Avalonia.Animation
/// <summary>
/// Handles the timing and lifetime of a <see cref="Transition{T}"/>.
/// </summary>
internal class TransitionInstance : SingleSubscriberObservableBase<double>
internal class TransitionInstance : SingleSubscriberObservableBase<double>, IObserver<TimeSpan>
{
private IDisposable _timerSubscription;
private TimeSpan _delay;
private TimeSpan _duration;
private readonly IClock _baseClock;
private IClock _clock;
private TransitionClock _clock;
public TransitionInstance(IClock clock, TimeSpan delay, TimeSpan duration)
{
@ -75,9 +72,56 @@ namespace Avalonia.Animation
protected override void Subscribed()
{
_clock = new Clock(_baseClock);
_timerSubscription = _clock.Subscribe(TimerTick);
_clock = new TransitionClock(_baseClock);
_timerSubscription = _clock.Subscribe(this);
PublishNext(0.0d);
}
void IObserver<TimeSpan>.OnCompleted()
{
PublishCompleted();
}
void IObserver<TimeSpan>.OnError(Exception error)
{
PublishError(error);
}
void IObserver<TimeSpan>.OnNext(TimeSpan value)
{
TimerTick(value);
}
/// <summary>
/// TODO: This clock is still fairly expensive due to <see cref="ClockBase"/> implementation.
/// </summary>
private sealed class TransitionClock : ClockBase, IObserver<TimeSpan>
{
private readonly IDisposable _parentSubscription;
public TransitionClock(IClock parent)
{
_parentSubscription = parent.Subscribe(this);
}
protected override void Stop()
{
_parentSubscription.Dispose();
}
void IObserver<TimeSpan>.OnNext(TimeSpan value)
{
Pulse(value);
}
void IObserver<TimeSpan>.OnCompleted()
{
}
void IObserver<TimeSpan>.OnError(Exception error)
{
ExceptionDispatchInfo.Capture(error).Throw();
}
}
}
}

58
src/Avalonia.Animation/TransitionObservableBase.cs

@ -0,0 +1,58 @@
using System;
using Avalonia.Animation.Easings;
using Avalonia.Reactive;
#nullable enable
namespace Avalonia.Animation
{
/// <summary>
/// Provides base for observables implementing transitions.
/// </summary>
/// <typeparam name="T">Type of the transitioned value.</typeparam>
public abstract class TransitionObservableBase<T> : SingleSubscriberObservableBase<T>, IObserver<double>
{
private readonly Easing _easing;
private readonly IObservable<double> _progress;
private IDisposable? _progressSubscription;
protected TransitionObservableBase(IObservable<double> progress, Easing easing)
{
_progress = progress;
_easing = easing;
}
/// <summary>
/// Produces value at given progress time point.
/// </summary>
/// <param name="progress">Transition progress.</param>
protected abstract T ProduceValue(double progress);
protected override void Subscribed()
{
_progressSubscription = _progress.Subscribe(this);
}
protected override void Unsubscribed()
{
_progressSubscription?.Dispose();
}
void IObserver<double>.OnCompleted()
{
PublishCompleted();
}
void IObserver<double>.OnError(Exception error)
{
PublishError(error);
}
void IObserver<double>.OnNext(double value)
{
double progress = _easing.Ease(value);
PublishNext(ProduceValue(progress));
}
}
}

15
src/Avalonia.Animation/Transitions/DoubleTransition.cs

@ -1,22 +1,11 @@
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="double"/> types.
/// </summary>
public class DoubleTransition : Transition<double>
public class DoubleTransition : AnimatorDrivenTransition<double, DoubleAnimator>
{
/// <inheritdocs/>
public override IObservable<double> DoTransition(IObservable<double> progress, double oldValue, double newValue)
{
return progress
.Select(p =>
{
var f = Easing.Ease(p);
return ((newValue - oldValue) * f) + oldValue;
});
}
}
}

12
src/Avalonia.Animation/Transitions/FloatTransition.cs

@ -1,19 +1,11 @@
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="float"/> types.
/// </summary>
public class FloatTransition : Transition<float>
public class FloatTransition : AnimatorDrivenTransition<float, FloatAnimator>
{
/// <inheritdocs/>
public override IObservable<float> DoTransition(IObservable<double> progress, float oldValue, float newValue)
{
var delta = newValue - oldValue;
return progress
.Select(p => (float)Easing.Ease(p) * delta + oldValue);
}
}
}

12
src/Avalonia.Animation/Transitions/IntegerTransition.cs

@ -1,19 +1,11 @@
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="int"/> types.
/// </summary>
public class IntegerTransition : Transition<int>
public class IntegerTransition : AnimatorDrivenTransition<int, Int32Animator>
{
/// <inheritdocs/>
public override IObservable<int> DoTransition(IObservable<double> progress, int oldValue, int newValue)
{
var delta = newValue - oldValue;
return progress
.Select(p => (int)(Easing.Ease(p) * delta + oldValue));
}
}
}

39
src/Avalonia.Base/AvaloniaObject.cs

@ -7,6 +7,8 @@ using Avalonia.Logging;
using Avalonia.PropertyStore;
using Avalonia.Threading;
#nullable enable
namespace Avalonia
{
/// <summary>
@ -17,12 +19,12 @@ namespace Avalonia
/// </remarks>
public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IValueSink
{
private IAvaloniaObject _inheritanceParent;
private List<IDisposable> _directBindings;
private PropertyChangedEventHandler _inpcChanged;
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
private List<IAvaloniaObject> _inheritanceChildren;
private ValueStore _values;
private IAvaloniaObject? _inheritanceParent;
private List<IDisposable>? _directBindings;
private PropertyChangedEventHandler? _inpcChanged;
private EventHandler<AvaloniaPropertyChangedEventArgs>? _propertyChanged;
private List<IAvaloniaObject>? _inheritanceChildren;
private ValueStore? _values;
private bool _batchUpdate;
/// <summary>
@ -36,7 +38,7 @@ namespace Avalonia
/// <summary>
/// Raised when a <see cref="AvaloniaProperty"/> value changes on this object.
/// </summary>
public event EventHandler<AvaloniaPropertyChangedEventArgs> PropertyChanged
public event EventHandler<AvaloniaPropertyChangedEventArgs>? PropertyChanged
{
add { _propertyChanged += value; }
remove { _propertyChanged -= value; }
@ -58,7 +60,7 @@ namespace Avalonia
/// <value>
/// The inheritance parent.
/// </value>
protected IAvaloniaObject InheritanceParent
protected IAvaloniaObject? InheritanceParent
{
get
{
@ -289,7 +291,8 @@ namespace Avalonia
/// <returns>True if the property is animating, otherwise false.</returns>
public bool IsAnimating(AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(property != null);
property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
return _values?.IsAnimating(property) ?? false;
@ -306,7 +309,8 @@ namespace Avalonia
/// </remarks>
public bool IsSet(AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(property != null);
property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
return _values?.IsSet(property) ?? false;
@ -320,7 +324,7 @@ namespace Avalonia
/// <param name="priority">The priority of the value.</param>
public void SetValue(
AvaloniaProperty property,
object value,
object? value,
BindingPriority priority = BindingPriority.LocalValue)
{
property = property ?? throw new ArgumentNullException(nameof(property));
@ -338,7 +342,7 @@ namespace Avalonia
/// <returns>
/// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null.
/// </returns>
public IDisposable SetValue<T>(
public IDisposable? SetValue<T>(
StyledPropertyBase<T> property,
T value,
BindingPriority priority = BindingPriority.LocalValue)
@ -497,7 +501,7 @@ namespace Avalonia
}
/// <inheritdoc/>
Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
Delegate[]? IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
{
return _propertyChanged?.GetInvocationList();
}
@ -723,7 +727,8 @@ namespace Avalonia
{
var values = o._values;
if (values?.TryGetValue(property, maxPriority, out value) == true)
if (values != null
&& values.TryGetValue(property, maxPriority, out value) == true)
{
return value;
}
@ -873,7 +878,7 @@ namespace Avalonia
}
else
{
LogBindingError(property, value.Error);
LogBindingError(property, value.Error!);
}
}
}
@ -907,14 +912,14 @@ namespace Avalonia
{
_owner = owner;
_property = property;
_owner._directBindings.Add(this);
_owner._directBindings!.Add(this);
_subscription = source.Subscribe(this);
}
public void Dispose()
{
_subscription.Dispose();
_owner._directBindings.Remove(this);
_owner._directBindings!.Remove(this);
}
public void OnCompleted() => Dispose();

24
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -585,6 +585,30 @@ namespace Avalonia
});
}
/// <summary>
/// Subscribes to a property changed notifications for changes that originate from a
/// <typeparamref name="TTarget"/>.
/// </summary>
/// <typeparam name="TTarget">The type of the property change sender.</typeparam>
/// /// <typeparam name="TValue">The type of the property..</typeparam>
/// <param name="observable">The property changed observable.</param>
/// <param name="action">
/// The method to call. The parameters are the sender and the event args.
/// </param>
/// <returns>A disposable that can be used to terminate the subscription.</returns>
public static IDisposable AddClassHandler<TTarget, TValue>(
this IObservable<AvaloniaPropertyChangedEventArgs<TValue>> observable,
Action<TTarget, AvaloniaPropertyChangedEventArgs<TValue>> action) where TTarget : AvaloniaObject
{
return observable.Subscribe(e =>
{
if (e.Sender is TTarget target)
{
action(target, e);
}
});
}
/// <summary>
/// Subscribes to a property changed notifications for changes that originate from a
/// <typeparamref name="TTarget"/>.

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

@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Utilities;
@ -358,6 +359,7 @@ namespace Avalonia.Data
e);
}
[Conditional("DEBUG")]
private static void ValidateValue([AllowNull] T value)
{
if (value is UnsetValueType)

2
src/Avalonia.Base/Data/Core/StreamNode.cs

@ -20,7 +20,7 @@ namespace Avalonia.Data.Core
protected override void StartListeningCore(WeakReference<object> reference)
{
GetPlugin(reference)?.Start(reference).Subscribe(ValueChanged);
_subscription = GetPlugin(reference)?.Start(reference).Subscribe(ValueChanged);
}
protected override void StopListeningCore()

4
src/Avalonia.Base/Diagnostics/IAvaloniaObjectDebug.cs

@ -1,5 +1,7 @@
using System;
#nullable enable
namespace Avalonia.Diagnostics
{
/// <summary>
@ -14,6 +16,6 @@ namespace Avalonia.Diagnostics
/// <returns>
/// The subscribers or null if no subscribers.
/// </returns>
Delegate[] GetPropertyChangedSubscribers();
Delegate[]? GetPropertyChangedSubscribers();
}
}

9
src/Avalonia.Base/IAvaloniaObject.cs

@ -1,6 +1,8 @@
using System;
using Avalonia.Data;
#nullable enable
namespace Avalonia
{
/// <summary>
@ -11,7 +13,7 @@ namespace Avalonia
/// <summary>
/// Raised when a <see cref="AvaloniaProperty"/> value changes on this object.
/// </summary>
event EventHandler<AvaloniaPropertyChangedEventArgs> PropertyChanged;
event EventHandler<AvaloniaPropertyChangedEventArgs>? PropertyChanged;
/// <summary>
/// Clears an <see cref="AvaloniaProperty"/>'s local value.
@ -75,7 +77,10 @@ namespace Avalonia
/// <param name="property">The property.</param>
/// <param name="value">The value.</param>
/// <param name="priority">The priority of the value.</param>
IDisposable SetValue<T>(
/// <returns>
/// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null.
/// </returns>
IDisposable? SetValue<T>(
StyledPropertyBase<T> property,
T value,
BindingPriority priority = BindingPriority.LocalValue);

81
src/Avalonia.Base/Metadata/NullableAttributes.cs

@ -1,6 +1,5 @@
#pragma warning disable MA0048 // File name must match type name
#define INTERNAL_NULLABLE_ATTRIBUTES
#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48
// https://github.com/dotnet/corefx/blob/48363ac826ccf66fbe31a5dcb1dc2aab9a7dd768/src/Common/src/CoreLib/System/Diagnostics/CodeAnalysis/NullableAttributes.cs
@ -10,6 +9,7 @@
namespace System.Diagnostics.CodeAnalysis
{
#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48
/// <summary>Specifies that null is allowed as an input even if the corresponding type disallows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
#if INTERNAL_NULLABLE_ATTRIBUTES
@ -136,5 +136,82 @@ namespace System.Diagnostics.CodeAnalysis
/// <summary>Gets the condition parameter value.</summary>
public bool ParameterValue { get; }
}
}
#endif // NETSTANDARD2_0 attributes
#if NETSTANDARD2_1 || NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48
/// <summary>
/// Specifies that the method or property will ensure that the listed field and property members have
/// not-<see langword="null"/> values.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
#if INTERNAL_NULLABLE_ATTRIBUTES
internal
#else
public
#endif
sealed class MemberNotNullAttribute : Attribute
{
/// <summary>Gets field or property member names.</summary>
public string[] Members { get; }
/// <summary>Initializes the attribute with a field or property member.</summary>
/// <param name="member">The field or property member that is promised to be not-null.</param>
public MemberNotNullAttribute(string member)
{
Members = new[] { member };
}
/// <summary>Initializes the attribute with the list of field and property members.</summary>
/// <param name="members">The list of field and property members that are promised to be not-null.</param>
public MemberNotNullAttribute(params string[] members)
{
Members = members;
}
}
/// <summary>
/// Specifies that the method or property will ensure that the listed field and property members have
/// non-<see langword="null"/> values when returning with the specified return value condition.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
#if INTERNAL_NULLABLE_ATTRIBUTES
internal
#else
public
#endif
sealed class MemberNotNullWhenAttribute : Attribute
{
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
/// <summary>Gets field or property member names.</summary>
public string[] Members { get; }
/// <summary>Initializes the attribute with the specified return value condition and a field or property member.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value,
/// the associated parameter will not be <see langword="null"/>.
/// </param>
/// <param name="member">The field or property member that is promised to be not-<see langword="null"/>.</param>
public MemberNotNullWhenAttribute(bool returnValue, string member)
{
ReturnValue = returnValue;
Members = new[] { member };
}
/// <summary>Initializes the attribute with the specified return value condition and list of field and property members.
/// </summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value,
/// the associated parameter will not be <see langword="null"/>.
/// </param>
/// <param name="members">The list of field and property members that are promised to be not-null.</param>
public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
{
ReturnValue = returnValue;
Members = members;
}
}
#endif // NETSTANDARD2_1 attributes
}

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

@ -79,7 +79,7 @@ namespace Avalonia.PropertyStore
public void OnError(Exception error)
{
throw new NotImplementedException();
throw new NotImplementedException("BindingEntry.OnError is not implemented", error);
}
public void OnNext(BindingValue<T> value)

9
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -75,7 +75,6 @@ namespace Avalonia.Controls
private const double DATAGRID_defaultMinColumnWidth = 20;
private const double DATAGRID_defaultMaxColumnWidth = double.PositiveInfinity;
private List<Exception> _validationErrors;
private List<Exception> _bindingValidationErrors;
private IDisposable _validationSubscription;
@ -102,7 +101,6 @@ namespace Avalonia.Controls
private bool _areHandlersSuspended;
private bool _autoSizingColumns;
private IndexToValueTable<bool> _collapsedSlotsTable;
private DataGridCellCoordinates _currentCellCoordinates;
private Control _clickedElement;
// used to store the current column during a Reset
@ -141,7 +139,6 @@ namespace Avalonia.Controls
private DataGridSelectedItemsCollection _selectedItems;
private bool _temporarilyResetCurrentCell;
private object _uneditedValue; // Represents the original current cell value at the time it enters editing mode.
private ICellEditBinding _currentCellEditBinding;
// An approximation of the sum of the heights in pixels of the scrolling rows preceding
// the first displayed scrolling row. Since the scrolled off rows are discarded, the grid
@ -535,7 +532,8 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect<DataGrid, int>(
nameof(SelectedIndex),
o => o.SelectedIndex,
(o, v) => o.SelectedIndex = v);
(o, v) => o.SelectedIndex = v,
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Gets or sets the index of the current selection.
@ -553,7 +551,8 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect<DataGrid, object>(
nameof(SelectedItem),
o => o.SelectedItem,
(o, v) => o.SelectedItem = v);
(o, v) => o.SelectedItem = v,
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Gets or sets the data item corresponding to the selected row.

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

@ -425,7 +425,7 @@ namespace Avalonia.Controls
UpdateDisplayedRows(DisplayData.FirstScrollingSlot, CellsHeight);
}
if (DisplayData.FirstScrollingSlot < slot && DisplayData.LastScrollingSlot > slot)
if (DisplayData.FirstScrollingSlot < slot && (DisplayData.LastScrollingSlot > slot || DisplayData.LastScrollingSlot == -1))
{
// The row is already displayed in its entirety
return true;

4
src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml

@ -163,6 +163,10 @@
<Style Selector="DataGridCell:invalid /template/ Rectangle#InvalidVisualElement">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="DataGridCell > TextBox DataValidationErrors">
<Setter Property="Template" Value="{DynamicResource TooltipDataValidationContentTemplate}" />
<Setter Property="ErrorTemplate" Value="{DynamicResource TooltipDataValidationErrorTemplate}" />
</Style>
<Style Selector="DataGridColumnHeader">
<Setter Property="Foreground" Value="{DynamicResource DataGridColumnHeaderForegroundBrush}" />

4
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -4,10 +4,12 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalon
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.IMenuItem.StaysOpenOnClick.set(System.Boolean)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseClosed()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract.
MembersMustExist : Member 'public Avalonia.StyledProperty<System.Boolean> Avalonia.StyledProperty<System.Boolean> Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract.
EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
Total Issues: 11
Total Issues: 13

31
src/Avalonia.Controls/Application.cs

@ -13,6 +13,7 @@ using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.Threading;
#nullable enable
namespace Avalonia
{
@ -35,27 +36,27 @@ namespace Avalonia
/// <summary>
/// The application-global data templates.
/// </summary>
private DataTemplates _dataTemplates;
private DataTemplates? _dataTemplates;
private readonly Lazy<IClipboard> _clipboard =
new Lazy<IClipboard>(() => (IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard)));
private readonly Styler _styler = new Styler();
private Styles _styles;
private IResourceDictionary _resources;
private Styles? _styles;
private IResourceDictionary? _resources;
private bool _notifyingResourcesChanged;
private Action<IReadOnlyList<IStyle>> _stylesAdded;
private Action<IReadOnlyList<IStyle>> _stylesRemoved;
private Action<IReadOnlyList<IStyle>>? _stylesAdded;
private Action<IReadOnlyList<IStyle>>? _stylesRemoved;
/// <summary>
/// Defines the <see cref="DataContext"/> property.
/// </summary>
public static readonly StyledProperty<object> DataContextProperty =
public static readonly StyledProperty<object?> DataContextProperty =
StyledElement.DataContextProperty.AddOwner<Application>();
/// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged;
public event EventHandler<UrlOpenedEventArgs> UrlsOpened;
public event EventHandler<UrlOpenedEventArgs>? UrlsOpened;
/// <summary>
/// Creates an instance of the <see cref="Application"/> class.
@ -72,7 +73,7 @@ namespace Avalonia
/// The data context property specifies the default object that will
/// be used for data binding.
/// </remarks>
public object DataContext
public object? DataContext
{
get { return GetValue(DataContextProperty); }
set { SetValue(DataContextProperty, value); }
@ -162,7 +163,7 @@ namespace Avalonia
/// <summary>
/// Gets the styling parent of the application, which is null.
/// </summary>
IStyleHost IStyleHost.StylingParent => null;
IStyleHost? IStyleHost.StylingParent => null;
/// <inheritdoc/>
bool IStyleHost.IsStylesInitialized => _styles != null;
@ -194,7 +195,7 @@ namespace Avalonia
public virtual void Initialize() { }
/// <inheritdoc/>
bool IResourceNode.TryGetResource(object key, out object value)
bool IResourceNode.TryGetResource(object key, out object? value)
{
value = null;
return (_resources?.TryGetResource(key, out value) ?? false) ||
@ -279,17 +280,17 @@ namespace Avalonia
NotifyResourcesChanged(e);
}
private string _name;
private string? _name;
/// <summary>
/// Defines Name property
/// </summary>
public static readonly DirectProperty<Application, string> NameProperty =
AvaloniaProperty.RegisterDirect<Application, string>("Name", o => o.Name, (o, v) => o.Name = v);
public static readonly DirectProperty<Application, string?> NameProperty =
AvaloniaProperty.RegisterDirect<Application, string?>("Name", o => o.Name, (o, v) => o.Name = v);
/// <summary>
/// Application name to be used for various platform-specific purposes
/// </summary>
public string Name
public string? Name
{
get => _name;
set => SetAndRaise(NameProperty, ref _name, value);

2
src/Avalonia.Controls/ComboBox.cs

@ -205,7 +205,7 @@ namespace Avalonia.Controls
if (e.Handled)
return;
if (e.Key == Key.F4 ||
if ((e.Key == Key.F4 && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) == false) ||
((e.Key == Key.Down || e.Key == Key.Up) && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt)))
{
IsDropDownOpen = !IsDropDownOpen;

2
src/Avalonia.Controls/Control.cs

@ -201,7 +201,7 @@ namespace Avalonia.Controls
{
base.OnLostFocus(e);
if (_focusAdorner != null)
if (_focusAdorner?.Parent != null)
{
var adornerLayer = (IPanel)_focusAdorner.Parent;
adornerLayer.Children.Remove(_focusAdorner);

44
src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs

@ -0,0 +1,44 @@
#nullable enable
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace Avalonia.Controls.Converters
{
/// <summary>
/// Converts an existing CornerRadius struct to a new CornerRadius struct,
/// with filters applied to extract only the specified fields, leaving the others set to 0.
/// </summary>
public class CornerRadiusFilterConverter : IValueConverter
{
/// <summary>
/// Gets or sets the type of the filter applied to the <see cref="CornerRadiusFilterConverter"/>.
/// </summary>
public CornerRadiusFilterKinds Filter { get; set; }
/// <summary>
/// Gets or sets the scale multiplier applied to the <see cref="CornerRadiusFilterConverter"/>.
/// </summary>
public double Scale { get; set; } = 1;
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (!(value is CornerRadius radius))
{
return value;
}
return new CornerRadius(
Filter.HasAllFlags(CornerRadiusFilterKinds.TopLeft) ? radius.TopLeft * Scale : 0,
Filter.HasAllFlags(CornerRadiusFilterKinds.TopRight) ? radius.TopRight * Scale : 0,
Filter.HasAllFlags(CornerRadiusFilterKinds.BottomRight) ? radius.BottomRight * Scale : 0,
Filter.HasAllFlags(CornerRadiusFilterKinds.BottomLeft) ? radius.BottomLeft * Scale : 0);
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

32
src/Avalonia.Controls/Converters/CornerRadiusFilterKind.cs

@ -0,0 +1,32 @@
using System;
namespace Avalonia.Controls.Converters
{
/// <summary>
/// Defines constants that specify the filter type for a <see cref="CornerRadiusFilterConverter"/> instance.
/// </summary>
[Flags]
public enum CornerRadiusFilterKinds
{
/// <summary>
/// No filter applied.
/// </summary>
None,
/// <summary>
/// Filters TopLeft value.
/// </summary>
TopLeft = 1,
/// <summary>
/// Filters TopRight value.
/// </summary>
TopRight = 2,
/// <summary>
/// Filters BottomLeft value.
/// </summary>
BottomLeft = 4,
/// <summary>
/// Filters BottomRight value.
/// </summary>
BottomRight = 8
}
}

4
src/Avalonia.Controls/DateTimePickers/DatePicker.cs

@ -2,6 +2,7 @@
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Interactivity;
using System;
using System.Collections.Generic;
@ -88,7 +89,8 @@ namespace Avalonia.Controls
/// </summary>
public static readonly DirectProperty<DatePicker, DateTimeOffset?> SelectedDateProperty =
AvaloniaProperty.RegisterDirect<DatePicker, DateTimeOffset?>(nameof(SelectedDate),
x => x.SelectedDate, (x, v) => x.SelectedDate = v);
x => x.SelectedDate, (x, v) => x.SelectedDate = v,
defaultBindingMode: BindingMode.TwoWay);
// Template Items
private Button _flyoutButton;

6
src/Avalonia.Controls/DateTimePickers/TimePicker.cs

@ -2,13 +2,14 @@
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using System;
using System.Globalization;
namespace Avalonia.Controls
{
/// <summary>
/// A control to allow the user to select a time
/// A control to allow the user to select a time.
/// </summary>
[PseudoClasses(":hasnotime")]
public class TimePicker : TemplatedControl
@ -44,7 +45,8 @@ namespace Avalonia.Controls
/// </summary>
public static readonly DirectProperty<TimePicker, TimeSpan?> SelectedTimeProperty =
AvaloniaProperty.RegisterDirect<TimePicker, TimeSpan?>(nameof(SelectedTime),
x => x.SelectedTime, (x, v) => x.SelectedTime = v);
x => x.SelectedTime, (x, v) => x.SelectedTime = v,
defaultBindingMode: BindingMode.TwoWay);
// Template Items
private TimePickerPresenter _presenter;

42
src/Avalonia.Controls/Expander.cs

@ -1,22 +1,47 @@
using System.Threading;
using Avalonia.Animation;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
/// Direction in which an <see cref="Expander"/> control opens.
/// </summary>
public enum ExpandDirection
{
/// <summary>
/// Opens down.
/// </summary>
Down,
/// <summary>
/// Opens up.
/// </summary>
Up,
/// <summary>
/// Opens left.
/// </summary>
Left,
/// <summary>
/// Opens right.
/// </summary>
Right
}
/// <summary>
/// A control with a header that has a collapsible content section.
/// </summary>
[PseudoClasses(":expanded", ":up", ":down", ":left", ":right")]
public class Expander : HeaderedContentControl
{
public static readonly StyledProperty<IPageTransition> ContentTransitionProperty =
AvaloniaProperty.Register<Expander, IPageTransition>(nameof(ContentTransition));
public static readonly StyledProperty<IPageTransition?> ContentTransitionProperty =
AvaloniaProperty.Register<Expander, IPageTransition?>(nameof(ContentTransition));
public static readonly StyledProperty<ExpandDirection> ExpandDirectionProperty =
AvaloniaProperty.Register<Expander, ExpandDirection>(nameof(ExpandDirection), ExpandDirection.Down);
@ -29,6 +54,7 @@ namespace Avalonia.Controls
defaultBindingMode: Data.BindingMode.TwoWay);
private bool _isExpanded;
private CancellationTokenSource? _lastTransitionCts;
static Expander()
{
@ -40,7 +66,7 @@ namespace Avalonia.Controls
UpdatePseudoClasses(ExpandDirection);
}
public IPageTransition ContentTransition
public IPageTransition? ContentTransition
{
get => GetValue(ContentTransitionProperty);
set => SetValue(ContentTransitionProperty, value);
@ -62,19 +88,23 @@ namespace Avalonia.Controls
}
}
protected virtual void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e)
protected virtual async void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e)
{
if (Content != null && ContentTransition != null && Presenter is Visual visualContent)
{
bool forward = ExpandDirection == ExpandDirection.Left ||
ExpandDirection == ExpandDirection.Up;
_lastTransitionCts?.Cancel();
_lastTransitionCts = new CancellationTokenSource();
if (IsExpanded)
{
ContentTransition.Start(null, visualContent, forward);
await ContentTransition.Start(null, visualContent, forward, _lastTransitionCts.Token);
}
else
{
ContentTransition.Start(visualContent, null, !forward);
await ContentTransition.Start(visualContent, null, forward, _lastTransitionCts.Token);
}
}
}

3
src/Avalonia.Controls/ExperimentalAcrylicBorder.cs

@ -3,6 +3,7 @@ using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
using System;
using Avalonia.Media.Immutable;
namespace Avalonia.Controls
{
@ -90,7 +91,7 @@ namespace Avalonia.Controls
}
else
{
_borderRenderHelper.Render(context, Bounds.Size, new Thickness(), CornerRadius, new SolidColorBrush(Material.FallbackColor), null, default);
_borderRenderHelper.Render(context, Bounds.Size, new Thickness(), CornerRadius, new ImmutableSolidColorBrush(Material.FallbackColor), null, default);
}
}

32
src/Avalonia.Controls/Flyouts/FlyoutBase.cs

@ -4,6 +4,7 @@ using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.Rendering;
#nullable enable
@ -51,8 +52,9 @@ namespace Avalonia.Controls.Primitives
private bool _isOpen;
private Control? _target;
private FlyoutShowMode _showMode = FlyoutShowMode.Standard;
private Rect? enlargedPopupRect;
private IDisposable? transientDisposable;
private Rect? _enlargedPopupRect;
private PixelRect? _enlargePopupRectScreenPixelRect;
private IDisposable? _transientDisposable;
protected Popup? Popup { get; private set; }
@ -163,8 +165,10 @@ namespace Avalonia.Controls.Primitives
Popup.IsOpen = false;
// Ensure this isn't active
transientDisposable?.Dispose();
transientDisposable = null;
_transientDisposable?.Dispose();
_transientDisposable = null;
_enlargedPopupRect = null;
_enlargePopupRectScreenPixelRect = null;
OnClosed();
}
@ -230,7 +234,7 @@ namespace Avalonia.Controls.Primitives
}
else if (ShowMode == FlyoutShowMode.TransientWithDismissOnPointerMoveAway)
{
transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss);
_transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss);
}
}
@ -246,20 +250,20 @@ namespace Avalonia.Controls.Primitives
// For windowed popups, enlargedPopupRect is in screen coordinates,
// for overlay popups, its in OverlayLayer coordinates
if (enlargedPopupRect == null)
if (_enlargedPopupRect == null && _enlargePopupRectScreenPixelRect == null)
{
// Only do this once when the Flyout opens & cache the result
if (Popup?.Host is PopupRoot root)
{
// Get the popup root bounds and convert to screen coordinates
var tmp = root.Bounds.Inflate(100);
var scPt = root.PointToScreen(tmp.TopLeft);
enlargedPopupRect = new Rect(scPt.X, scPt.Y, tmp.Width, tmp.Height);
_enlargePopupRectScreenPixelRect = new PixelRect(root.PointToScreen(tmp.TopLeft), root.PointToScreen(tmp.BottomRight));
}
else if (Popup?.Host is OverlayPopupHost host)
{
// Overlay popups are in OverlayLayer coordinates, just use that
enlargedPopupRect = host.Bounds.Inflate(100);
_enlargedPopupRect = host.Bounds.Inflate(100);
}
return;
@ -273,24 +277,18 @@ namespace Avalonia.Controls.Primitives
// window will not close this (as pointer events stop), which
// does match UWP
var pt = pArgs.Root.PointToScreen(pArgs.Position);
if (!enlargedPopupRect?.Contains(new Point(pt.X, pt.Y)) ?? false)
if (!_enlargePopupRectScreenPixelRect?.Contains(pt) ?? false)
{
HideCore(false);
enlargedPopupRect = null;
transientDisposable?.Dispose();
transientDisposable = null;
}
}
else if (Popup?.Host is OverlayPopupHost)
{
// Same as above here, but just different coordinate space
// so we don't need to translate
if (!enlargedPopupRect?.Contains(pArgs.Position) ?? false)
if (!_enlargedPopupRect?.Contains(pArgs.Position) ?? false)
{
HideCore(false);
enlargedPopupRect = null;
transientDisposable?.Dispose();
transientDisposable = null;
}
}
}

3
src/Avalonia.Controls/Grid.cs

@ -17,7 +17,7 @@ using Avalonia.VisualTree;
namespace Avalonia.Controls
{
/// <summary>
/// Grid
/// Defines a flexible grid area that consists of columns and rows.
/// </summary>
public class Grid : Panel
{
@ -978,6 +978,7 @@ namespace Avalonia.Controls
/// width is not registered in columns.</param>
/// <param name="forceInfinityV">Passed through to MeasureCell.
/// When "true" cells' desired height is not registered in rows.</param>
/// <param name="hasDesiredSizeUChanged">return true when desired size has changed</param>
private void MeasureCellsGroup(
int cellsHead,
Size referenceSize,

6
src/Avalonia.Controls/GridLength.cs

@ -77,6 +77,12 @@ namespace Avalonia.Controls
/// </summary>
public static GridLength Auto => new GridLength(0, GridUnitType.Auto);
/// <summary>
/// Gets an instance of <see cref="GridLength"/> that indicates that a row or column should
/// fill its content.
/// </summary>
public static GridLength Star => new GridLength(1, GridUnitType.Star);
/// <summary>
/// Gets the unit of the <see cref="GridLength"/>.
/// </summary>

4
src/Avalonia.Controls/IControl.cs

@ -3,6 +3,8 @@ using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
@ -15,6 +17,6 @@ namespace Avalonia.Controls
INamed,
IStyledElement
{
new IControl Parent { get; }
new IControl? Parent { get; }
}
}

6
src/Avalonia.Controls/Image.cs

@ -1,5 +1,6 @@
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Metadata;
namespace Avalonia.Controls
{
@ -30,13 +31,14 @@ namespace Avalonia.Controls
static Image()
{
AffectsRender<Image>(SourceProperty, StretchProperty);
AffectsMeasure<Image>(SourceProperty, StretchProperty);
AffectsRender<Image>(SourceProperty, StretchProperty, StretchDirectionProperty);
AffectsMeasure<Image>(SourceProperty, StretchProperty, StretchDirectionProperty);
}
/// <summary>
/// Gets or sets the image that will be displayed.
/// </summary>
[Content]
public IImage Source
{
get { return GetValue(SourceProperty); }

1
src/Avalonia.Controls/Menu.cs

@ -17,7 +17,6 @@ namespace Avalonia.Controls
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Horizontal });
private LightDismissOverlayLayer? _overlay;
/// <summary>
/// Initializes a new instance of the <see cref="Menu"/> class.

6
src/Avalonia.Controls/MenuItem.cs

@ -36,7 +36,7 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="HotKey"/> property.
/// </summary>
public static readonly StyledProperty<KeyGesture> HotKeyProperty =
public static readonly StyledProperty<KeyGesture?> HotKeyProperty =
HotKeyManager.HotKeyProperty.AddOwner<MenuItem>();
/// <summary>
@ -108,7 +108,7 @@ namespace Avalonia.Controls
private ICommand? _command;
private bool _commandCanExecute = true;
private Popup? _popup;
private KeyGesture _hotkey;
private KeyGesture? _hotkey;
private bool _isEmbeddedInMenu;
/// <summary>
@ -214,7 +214,7 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets an <see cref="KeyGesture"/> associated with this control
/// </summary>
public KeyGesture HotKey
public KeyGesture? HotKey
{
get { return GetValue(HotKeyProperty); }
set { SetValue(HotKeyProperty, value); }

2
src/Avalonia.Controls/Presenters/CarouselPresenter.cs

@ -186,7 +186,7 @@ namespace Avalonia.Controls.Presenters
if (PageTransition != null && (from != null || to != null))
{
await PageTransition.Start((Visual)from, (Visual)to, fromIndex < toIndex);
await PageTransition.Start((Visual)from, (Visual)to, fromIndex < toIndex, default);
}
else if (to != null)
{

2
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -511,8 +511,8 @@ namespace Avalonia.Controls.Presenters
else if (scrollable.IsLogicalScrollEnabled)
{
Viewport = scrollable.Viewport;
Extent = scrollable.Extent;
Offset = scrollable.Offset;
Extent = scrollable.Extent;
}
}

22
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -6,6 +6,7 @@ using Avalonia.Metadata;
using Avalonia.Threading;
using Avalonia.VisualTree;
using Avalonia.Layout;
using Avalonia.Media.Immutable;
namespace Avalonia.Controls.Presenters
{
@ -360,32 +361,31 @@ namespace Avalonia.Controls.Presenters
RenderInternal(context);
if (selectionStart == selectionEnd)
if (selectionStart == selectionEnd && _caretBlink)
{
var caretBrush = CaretBrush;
var caretBrush = CaretBrush?.ToImmutable();
if (caretBrush is null)
{
var backgroundColor = (Background as SolidColorBrush)?.Color;
var backgroundColor = (Background as ISolidColorBrush)?.Color;
if (backgroundColor.HasValue)
{
byte red = (byte)~(backgroundColor.Value.R);
byte green = (byte)~(backgroundColor.Value.G);
byte blue = (byte)~(backgroundColor.Value.B);
caretBrush = new SolidColorBrush(Color.FromRgb(red, green, blue));
caretBrush = new ImmutableSolidColorBrush(Color.FromRgb(red, green, blue));
}
else
{
caretBrush = Brushes.Black;
}
}
if (_caretBlink)
{
var (p1, p2) = GetCaretPoints();
context.DrawLine(
new Pen(caretBrush, 1),
p1, p2);
}
var (p1, p2) = GetCaretPoints();
context.DrawLine(
new ImmutablePen(caretBrush, 1),
p1, p2);
}
}

22
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Specialized;
using System.Linq;
using System.Resources;
using Avalonia.Media;
using Avalonia.Rendering;
using Avalonia.Utilities;
@ -45,10 +46,27 @@ namespace Avalonia.Controls.Primitives
?.AdornerLayer;
}
protected override Size ArrangeOverride(Size finalSize)
protected override Size MeasureOverride(Size availableSize)
{
var parent = Parent;
foreach (var child in Children)
{
var info = child.GetValue(s_adornedElementInfoProperty);
if (info != null && info.Bounds.HasValue)
{
child.Measure(info.Bounds.Value.Bounds.Size);
}
else
{
child.Measure(availableSize);
}
}
return default;
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (var child in Children)
{
var info = child.GetValue(s_adornedElementInfoProperty);

3
src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs

@ -52,8 +52,7 @@ namespace Avalonia.Controls.Primitives
{
if (InputPassThroughElement is object)
{
var p = point.Transform(this.TransformToVisual(VisualRoot)!.Value);
var hit = VisualRoot.GetVisualAt(p, x => x != this);
var hit = VisualRoot.GetVisualAt(point, x => x != this);
if (hit is object)
{

18
src/Avalonia.Controls/Primitives/ScrollBarVisibility.cs

@ -1,10 +1,28 @@
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Specifies the visibility of a <see cref="ScrollBar"/> for scrollable content.
/// </summary>
public enum ScrollBarVisibility
{
/// <summary>
/// No scrollbars and no scrolling in this dimension.
/// </summary>
Disabled,
/// <summary>
/// The scrollbar should be visible only if there is more content than fits in the viewport.
/// </summary>
Auto,
/// <summary>
/// The scrollbar should never be visible. No space should ever be reserved for the scrollbar.
/// </summary>
Hidden,
/// <summary>
/// The scrollbar should always be visible. Space should always be reserved for the scrollbar.
/// </summary>
Visible,
}
}

3
src/Avalonia.Controls/RadioButton.cs

@ -8,6 +8,9 @@ using Avalonia.VisualTree;
namespace Avalonia.Controls
{
/// <summary>
/// Represents a button that allows a user to select a single option from a group of options.
/// </summary>
public class RadioButton : ToggleButton
{
private class RadioButtonGroupManager

3
src/Avalonia.Controls/RelativePanel.cs

@ -8,6 +8,9 @@ using Avalonia.Layout;
namespace Avalonia.Controls
{
/// <summary>
/// Defines an area within which you can position and align child objects in relation to each other or the parent panel.
/// </summary>
public partial class RelativePanel : Panel
{
private readonly Graph _childGraph;

2
src/Avalonia.Controls/Remote/RemoteWidget.cs

@ -70,7 +70,7 @@ namespace Avalonia.Controls.Remote
public override void Render(DrawingContext context)
{
if (_lastFrame != null)
if (_lastFrame != null && _lastFrame.Width != 0 && _lastFrame.Height != 0)
{
var fmt = (PixelFormat) _lastFrame.Format;
if (_bitmap == null || _bitmap.PixelSize.Width != _lastFrame.Width ||

3
src/Avalonia.Controls/RepeatButton.cs

@ -4,6 +4,9 @@ using Avalonia.Threading;
namespace Avalonia.Controls
{
/// <summary>
/// Represents a control that raises its <see cref="Button.Click"/> event repeatedly when it is pressed and held.
/// </summary>
public class RepeatButton : Button
{
/// <summary>

1
src/Avalonia.Controls/Repeater/ViewportManager.cs

@ -27,7 +27,6 @@ namespace Avalonia.Controls
private IScrollAnchorProvider _scroller;
private IControl _makeAnchorElement;
private bool _isAnchorOutsideRealizedRange;
private Task _cacheBuildAction;
private Rect _visibleWindow;
private Rect _layoutExtent;
// This is the expected shift by the layout.

1
src/Avalonia.Controls/Repeater/VirtualizationInfo.cs

@ -27,7 +27,6 @@ namespace Avalonia.Controls
internal class VirtualizationInfo
{
private int _pinCounter;
private object _data;
public Rect ArrangeBounds { get; set; }
public bool AutoRecycleCandidate { get; set; }

8
src/Avalonia.Controls/RowDefinitions.cs

@ -1,5 +1,4 @@
using System.Linq;
using Avalonia.Collections;
namespace Avalonia.Controls
{
@ -25,6 +24,11 @@ namespace Avalonia.Controls
AddRange(GridLength.ParseLengths(s).Select(x => new RowDefinition(x)));
}
public override string ToString()
{
return string.Join(",", this.Select(x => x.Height));
}
/// <summary>
/// Parses a string representation of row definitions collection.
/// </summary>
@ -32,4 +36,4 @@ namespace Avalonia.Controls
/// <returns>The <see cref="RowDefinitions"/>.</returns>
public static RowDefinitions Parse(string s) => new RowDefinitions(s);
}
}
}

36
src/Avalonia.Controls/ScrollViewer.cs

@ -176,8 +176,10 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="AllowAutoHide"/> property.
/// </summary>
public static readonly StyledProperty<bool> AllowAutoHideProperty =
ScrollBar.AllowAutoHideProperty.AddOwner<ScrollViewer>();
public static readonly AttachedProperty<bool> AllowAutoHideProperty =
AvaloniaProperty.RegisterAttached<ScrollViewer, Control, bool>(
nameof(AllowAutoHide),
true);
/// <summary>
/// Defines the <see cref="ScrollChanged"/> event.
@ -207,8 +209,8 @@ namespace Avalonia.Controls
/// </summary>
static ScrollViewer()
{
HorizontalScrollBarVisibilityProperty.Changed.AddClassHandler<ScrollViewer>((x, e) => x.ScrollBarVisibilityChanged(e));
VerticalScrollBarVisibilityProperty.Changed.AddClassHandler<ScrollViewer>((x, e) => x.ScrollBarVisibilityChanged(e));
HorizontalScrollBarVisibilityProperty.Changed.AddClassHandler<ScrollViewer, ScrollBarVisibility>((x, e) => x.ScrollBarVisibilityChanged(e));
VerticalScrollBarVisibilityProperty.Changed.AddClassHandler<ScrollViewer, ScrollBarVisibility>((x, e) => x.ScrollBarVisibilityChanged(e));
}
/// <summary>
@ -526,6 +528,26 @@ namespace Avalonia.Controls
return control.GetValue(VerticalScrollBarVisibilityProperty);
}
/// <summary>
/// Gets the value of the AllowAutoHideProperty attached property.
/// </summary>
/// <param name="control">The control to set the value on.</param>
/// <param name="value">The value of the property.</param>
public static void SetAllowAutoHide(Control control, bool value)
{
control.SetValue(AllowAutoHideProperty, value);
}
/// <summary>
/// Gets the value of the AllowAutoHideProperty attached property.
/// </summary>
/// <param name="control">The control to read the value from.</param>
/// <returns>The value of the property.</returns>
public static bool GetAllowAutoHide(Control control)
{
return control.GetValue(AllowAutoHideProperty);
}
/// <summary>
/// Gets the value of the VerticalScrollBarVisibility attached property.
/// </summary>
@ -604,10 +626,10 @@ namespace Avalonia.Controls
CalculatedPropertiesChanged();
}
private void ScrollBarVisibilityChanged(AvaloniaPropertyChangedEventArgs e)
private void ScrollBarVisibilityChanged(AvaloniaPropertyChangedEventArgs<ScrollBarVisibility> e)
{
var wasEnabled = !ScrollBarVisibility.Disabled.Equals(e.OldValue);
var isEnabled = !ScrollBarVisibility.Disabled.Equals(e.NewValue);
var wasEnabled = e.OldValue.GetValueOrDefault() != ScrollBarVisibility.Disabled;
var isEnabled = e.NewValue.GetValueOrDefault() != ScrollBarVisibility.Disabled;
if (wasEnabled != isEnabled)
{

26
src/Avalonia.Controls/Shapes/Shape.cs

@ -1,6 +1,7 @@
using System;
using Avalonia.Collections;
using Avalonia.Media;
using Avalonia.Media.Immutable;
#nullable enable
@ -199,8 +200,29 @@ namespace Avalonia.Controls.Shapes
if (geometry != null)
{
var pen = new Pen(Stroke, StrokeThickness, new DashStyle(StrokeDashArray, StrokeDashOffset),
StrokeLineCap, StrokeJoin);
var stroke = Stroke;
ImmutablePen? pen = null;
if (stroke != null)
{
var strokeDashArray = StrokeDashArray;
ImmutableDashStyle? dashStyle = null;
if (strokeDashArray != null && strokeDashArray.Count > 0)
{
dashStyle = new ImmutableDashStyle(strokeDashArray, StrokeDashOffset);
}
pen = new ImmutablePen(
stroke.ToImmutable(),
StrokeThickness,
dashStyle,
StrokeLineCap,
StrokeJoin);
}
context.DrawGeometry(Fill, pen, geometry);
}
}

2
src/Avalonia.Controls/SplitView.cs

@ -133,7 +133,7 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<SplitView, object?>(nameof(Pane));
/// <summary>
/// Defines the <see cref="HeaderTemplate"/> property.
/// Defines the <see cref="PaneTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate?> PaneTemplateProperty =
AvaloniaProperty.Register<HeaderedContentControl, IDataTemplate?>(nameof(PaneTemplate));

129
src/Avalonia.Controls/TextBox.cs

@ -17,6 +17,9 @@ using Avalonia.Controls.Metadata;
namespace Avalonia.Controls
{
/// <summary>
/// Represents a control that can be used to display or edit unformatted text.
/// </summary>
[PseudoClasses(":empty")]
public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.IUndoRedoHost
{
@ -116,9 +119,9 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<TextBox, bool>(nameof(RevealPassword));
public static readonly DirectProperty<TextBox, bool> CanCutProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanCut),
o => o.CanCut);
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanCut),
o => o.CanCut);
public static readonly DirectProperty<TextBox, bool> CanCopyProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
@ -126,9 +129,21 @@ namespace Avalonia.Controls
o => o.CanCopy);
public static readonly DirectProperty<TextBox, bool> CanPasteProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanPaste),
o => o.CanPaste);
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanPaste),
o => o.CanPaste);
public static readonly StyledProperty<bool> IsUndoEnabledProperty =
AvaloniaProperty.Register<TextBox, bool>(
nameof(IsUndoEnabled),
defaultValue: true);
public static readonly DirectProperty<TextBox, int> UndoLimitProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>(
nameof(UndoLimit),
o => o.UndoLimit,
(o, v) => o.UndoLimit = v,
unsetValue: -1);
struct UndoRedoState : IEquatable<UndoRedoState>
{
@ -215,7 +230,7 @@ namespace Avalonia.Controls
value = CoerceCaretIndex(value);
SetAndRaise(CaretIndexProperty, ref _caretIndex, value);
UndoRedoState state;
if (_undoRedoHelper.TryGetLastState(out state) && state.Text == Text)
if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text)
_undoRedoHelper.UpdateLastState();
}
}
@ -260,7 +275,11 @@ namespace Avalonia.Controls
set
{
value = CoerceCaretIndex(value);
SetAndRaise(SelectionStartProperty, ref _selectionStart, value);
var changed = SetAndRaise(SelectionStartProperty, ref _selectionStart, value);
if (changed)
{
UpdateCommandStates();
}
if (SelectionStart == SelectionEnd)
{
CaretIndex = SelectionStart;
@ -278,7 +297,11 @@ namespace Avalonia.Controls
set
{
value = CoerceCaretIndex(value);
SetAndRaise(SelectionEndProperty, ref _selectionEnd, value);
var changed = SetAndRaise(SelectionEndProperty, ref _selectionEnd, value);
if (changed)
{
UpdateCommandStates();
}
if (SelectionStart == SelectionEnd)
{
CaretIndex = SelectionEnd;
@ -305,7 +328,7 @@ namespace Avalonia.Controls
SelectionEnd = CoerceCaretIndex(SelectionEnd, value);
CaretIndex = CoerceCaretIndex(caretIndex, value);
if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing)
if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing)
{
_undoRedoHelper.Clear();
}
@ -318,7 +341,7 @@ namespace Avalonia.Controls
get { return GetSelection(); }
set
{
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
if (string.IsNullOrEmpty(value))
{
DeleteSelection();
@ -327,7 +350,7 @@ namespace Avalonia.Controls
{
HandleTextInput(value);
}
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
}
}
@ -435,6 +458,36 @@ namespace Avalonia.Controls
private set { SetAndRaise(CanPasteProperty, ref _canPaste, value); }
}
/// <summary>
/// Property for determining whether undo/redo is enabled
/// </summary>
public bool IsUndoEnabled
{
get { return GetValue(IsUndoEnabledProperty); }
set { SetValue(IsUndoEnabledProperty, value); }
}
public int UndoLimit
{
get { return _undoRedoHelper.Limit; }
set
{
if (_undoRedoHelper.Limit != value)
{
// can't use SetAndRaise due to using _undoRedoHelper.Limit
// (can't send a ref of a property to SetAndRaise),
// so use RaisePropertyChanged instead.
var oldValue = _undoRedoHelper.Limit;
_undoRedoHelper.Limit = value;
RaisePropertyChanged(UndoLimitProperty, oldValue, value);
}
// from docs at
// https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled:
// "Setting UndoLimit clears the undo queue."
_undoRedoHelper.Clear();
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
@ -454,6 +507,15 @@ namespace Avalonia.Controls
UpdatePseudoclasses();
UpdateCommandStates();
}
else if (change.Property == IsUndoEnabledProperty && change.NewValue.GetValueOrDefault<bool>() == false)
{
// from docs at
// https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled:
// "Setting this property to false clears the undo stack.
// Therefore, if you disable undo and then re-enable it, undo commands still do not work
// because the undo stack was emptied when you disabled undo."
_undoRedoHelper.Clear();
}
}
private void UpdateCommandStates()
@ -540,7 +602,10 @@ namespace Avalonia.Controls
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
CaretIndex += input.Length;
ClearSelection();
_undoRedoHelper.DiscardRedo();
if (IsUndoEnabled)
{
_undoRedoHelper.DiscardRedo();
}
}
}
@ -559,10 +624,10 @@ namespace Avalonia.Controls
var text = GetSelection();
if (text is null) return;
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
Copy();
DeleteSelection();
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
}
public async void Copy()
@ -580,9 +645,9 @@ namespace Avalonia.Controls
if (text is null) return;
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
HandleTextInput(text);
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
}
protected override void OnKeyDown(KeyEventArgs e)
@ -627,7 +692,7 @@ namespace Avalonia.Controls
Paste();
handled = true;
}
else if (Match(keymap.Undo))
else if (Match(keymap.Undo) && IsUndoEnabled)
{
try
{
@ -641,7 +706,7 @@ namespace Avalonia.Controls
handled = true;
}
else if (Match(keymap.Redo))
else if (Match(keymap.Redo) && IsUndoEnabled)
{
try
{
@ -741,7 +806,7 @@ namespace Avalonia.Controls
break;
case Key.Back:
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{
SetSelectionForControlBackspace();
@ -765,13 +830,13 @@ namespace Avalonia.Controls
CaretIndex -= removedCharacters;
ClearSelection();
}
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
handled = true;
break;
case Key.Delete:
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{
SetSelectionForControlDelete();
@ -793,7 +858,7 @@ namespace Avalonia.Controls
SetTextInternal(text.Substring(0, caretIndex) +
text.Substring(caretIndex + removedCharacters));
}
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
handled = true;
break;
@ -801,9 +866,9 @@ namespace Avalonia.Controls
case Key.Enter:
if (AcceptsReturn)
{
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
HandleTextInput(NewLine);
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
handled = true;
}
@ -812,9 +877,9 @@ namespace Avalonia.Controls
case Key.Tab:
if (AcceptsTab)
{
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
HandleTextInput("\t");
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
handled = true;
}
else
@ -1225,7 +1290,7 @@ namespace Avalonia.Controls
private void UpdatePseudoclasses()
{
PseudoClasses.Set(":empty", string.IsNullOrWhiteSpace(Text));
PseudoClasses.Set(":empty", string.IsNullOrEmpty(Text));
}
private bool IsPasswordBox => PasswordChar != default(char);
@ -1240,5 +1305,13 @@ namespace Avalonia.Controls
ClearSelection();
}
}
private void SnapshotUndoRedo()
{
if (IsUndoEnabled)
{
_undoRedoHelper.Snapshot();
}
}
}
}

3
src/Avalonia.Controls/TickBar.cs

@ -1,6 +1,7 @@
using Avalonia.Collections;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Utilities;
namespace Avalonia.Controls
@ -295,7 +296,7 @@ namespace Avalonia.Controls
endPoint = pt;
}
var pen = new Pen(Fill, 1.0d);
var pen = new ImmutablePen(Fill?.ToImmutable(), 1.0d);
// Is it Vertical?
if (Placement == TickBarPlacement.Left || Placement == TickBarPlacement.Right)

5
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@ -1,5 +1,6 @@
using System;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.Utilities;
@ -114,9 +115,9 @@ namespace Avalonia.Controls.Utils
var borderThickness = _borderThickness.Top;
IPen pen = null;
if (borderThickness > 0)
if (borderBrush != null && borderThickness > 0)
{
pen = new Pen(borderBrush, borderThickness);
pen = new ImmutablePen(borderBrush.ToImmutable(), borderThickness);
}
var rect = new Rect(_size);

11
src/Avalonia.Controls/Utils/UndoRedoHelper.cs

@ -22,6 +22,10 @@ namespace Avalonia.Controls.Utils
private LinkedListNode<TState> _currentNode;
/// <summary>
/// Maximum number of states this helper can store for undo/redo.
/// If -1, no limit is imposed.
/// </summary>
public int Limit { get; set; } = 10;
public UndoRedoHelper(IUndoRedoHost host)
@ -54,7 +58,10 @@ namespace Avalonia.Controls.Utils
public bool HasState => _currentNode != null;
public void UpdateLastState(TState state)
{
_states.Last.Value = state;
if (_states.Last != null)
{
_states.Last.Value = state;
}
}
public void UpdateLastState()
@ -86,7 +93,7 @@ namespace Avalonia.Controls.Utils
DiscardRedo();
_states.AddLast(current);
_currentNode = _states.Last;
if (_states.Count > Limit)
if (Limit != -1 && _states.Count > Limit)
_states.RemoveFirst();
}
}

87
src/Avalonia.Controls/Viewbox.cs

@ -1,40 +1,48 @@
using System;
using Avalonia.Media;
using Avalonia.Media;
namespace Avalonia.Controls
{
/// <summary>
/// Viewbox is used to scale single child.
/// Viewbox is used to scale single child to fit in the available space.
/// </summary>
/// <seealso cref="Avalonia.Controls.Decorator" />
public class Viewbox : Decorator
{
/// <summary>
/// The stretch property
/// Defines the <see cref="Stretch"/> property.
/// </summary>
public static readonly AvaloniaProperty<Stretch> StretchProperty =
AvaloniaProperty.RegisterDirect<Viewbox, Stretch>(nameof(Stretch),
v => v.Stretch, (c, v) => c.Stretch = v, Stretch.Uniform);
public static readonly StyledProperty<Stretch> StretchProperty =
AvaloniaProperty.Register<Image, Stretch>(nameof(Stretch), Stretch.Uniform);
private Stretch _stretch = Stretch.Uniform;
/// <summary>
/// Defines the <see cref="StretchDirection"/> property.
/// </summary>
public static readonly StyledProperty<StretchDirection> StretchDirectionProperty =
AvaloniaProperty.Register<Viewbox, StretchDirection>(nameof(StretchDirection), StretchDirection.Both);
static Viewbox()
{
ClipToBoundsProperty.OverrideDefaultValue<Viewbox>(true);
AffectsMeasure<Viewbox>(StretchProperty, StretchDirectionProperty);
}
/// <summary>
/// Gets or sets the stretch mode,
/// which determines how child fits into the available space.
/// </summary>
/// <value>
/// The stretch.
/// </value>
public Stretch Stretch
{
get => _stretch;
set => SetAndRaise(StretchProperty, ref _stretch, value);
get => GetValue(StretchProperty);
set => SetValue(StretchProperty, value);
}
static Viewbox()
/// <summary>
/// Gets or sets a value controlling in what direction contents will be stretched.
/// </summary>
public StretchDirection StretchDirection
{
ClipToBoundsProperty.OverrideDefaultValue<Viewbox>(true);
AffectsMeasure<Viewbox>(StretchProperty);
get => GetValue(StretchDirectionProperty);
set => SetValue(StretchDirectionProperty, value);
}
protected override Size MeasureOverride(Size availableSize)
@ -47,9 +55,9 @@ namespace Avalonia.Controls
var childSize = child.DesiredSize;
var scale = GetScale(availableSize, childSize, Stretch);
var size = Stretch.CalculateSize(availableSize, childSize, StretchDirection);
return (childSize * scale).Constrain(availableSize);
return size.Constrain(availableSize);
}
return new Size();
@ -62,7 +70,9 @@ namespace Avalonia.Controls
if (child != null)
{
var childSize = child.DesiredSize;
var scale = GetScale(finalSize, childSize, Stretch);
var scale = Stretch.CalculateScaling(finalSize, childSize, StretchDirection);
// TODO: Viewbox should have another decorator as a child so we won't affect other render transforms.
var scaleTransform = child.RenderTransform as ScaleTransform;
if (scaleTransform == null)
@ -81,44 +91,5 @@ namespace Avalonia.Controls
return new Size();
}
private static Vector GetScale(Size availableSize, Size childSize, Stretch stretch)
{
double scaleX = 1.0;
double scaleY = 1.0;
bool validWidth = !double.IsPositiveInfinity(availableSize.Width);
bool validHeight = !double.IsPositiveInfinity(availableSize.Height);
if (stretch != Stretch.None && (validWidth || validHeight))
{
scaleX = childSize.Width <= 0.0 ? 0.0 : availableSize.Width / childSize.Width;
scaleY = childSize.Height <= 0.0 ? 0.0 : availableSize.Height / childSize.Height;
if (!validWidth)
{
scaleX = scaleY;
}
else if (!validHeight)
{
scaleY = scaleX;
}
else
{
switch (stretch)
{
case Stretch.Uniform:
scaleX = scaleY = Math.Min(scaleX, scaleY);
break;
case Stretch.UniformToFill:
scaleX = scaleY = Math.Max(scaleX, scaleY);
break;
}
}
}
return new Vector(scaleX, scaleY);
}
}
}

12
src/Avalonia.Controls/Window.cs

@ -668,10 +668,11 @@ namespace Avalonia.Controls
Owner = parent;
parent?.AddChild(this, false);
PlatformImpl?.Show(ShowActivated);
Renderer?.Start();
SetWindowStartupLocation(Owner?.PlatformImpl);
PlatformImpl?.Show(ShowActivated);
Renderer?.Start();
}
OnOpened(EventArgs.Empty);
}
@ -739,6 +740,9 @@ namespace Avalonia.Controls
PlatformImpl?.SetParent(owner.PlatformImpl);
Owner = owner;
owner.AddChild(this, true);
SetWindowStartupLocation(owner.PlatformImpl);
PlatformImpl?.Show(ShowActivated);
Renderer?.Start();
@ -756,8 +760,6 @@ namespace Avalonia.Controls
OnOpened(EventArgs.Empty);
}
SetWindowStartupLocation(owner.PlatformImpl);
return result.Task;
}

4
src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj

@ -3,12 +3,16 @@
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>Avalonia</RootNamespace>
<PackageId>Avalonia.Diagnostics</PackageId>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Compile Update="**\*.xaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />

103
src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml

@ -0,0 +1,103 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Avalonia.Diagnostics.Controls"
x:CompileBindings="True"
x:DataType="controls:FilterTextBox">
<Design.PreviewWith>
<Border Padding="10">
<controls:FilterTextBox Width="200" Text="Hello World" />
</Border>
</Design.PreviewWith>
<Style Selector="TextBox.filter-text-box">
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="InnerRightContent">
<Template>
<StackPanel Orientation="Horizontal" Spacing="1">
<Button Margin="0,0,2,0"
Classes="textBoxClearButton"
ToolTip.Tip="Clear"
Cursor="Hand"
Command="{ReflectionBinding $parent[TextBox].Clear}"
Opacity="0.5" />
<ToggleButton Classes="filter-text-box-toggle"
ToolTip.Tip="Match Case"
IsChecked="{Binding $parent[controls:FilterTextBox].UseCaseSensitiveFilter}">
<Image>
<DrawingImage>
<GeometryDrawing Brush="{Binding $parent[ToggleButton].Foreground}">
<GeometryDrawing.Geometry>
M7.495 9.052L8.386 11.402H9.477L6.237 3H5.217L2 11.402H3.095L3.933 9.052H7.495ZM5.811 4.453L5.855 4.588L7.173 8.162H4.255L5.562 4.588L5.606 4.453L5.644 4.297L5.676 4.145L5.697 4.019H5.72L5.744 4.145L5.773 4.297L5.811 4.453ZM13.795 10.464V11.4H14.755V7.498C14.755 6.779 14.575 6.226 14.216 5.837C13.857 5.448 13.327 5.254 12.628 5.254C12.429 5.254 12.227 5.273 12.022 5.31C11.817 5.347 11.622 5.394 11.439 5.451C11.256 5.508 11.091 5.569 10.944 5.636C10.797 5.703 10.683 5.765 10.601 5.824V6.808C10.867 6.578 11.167 6.397 11.505 6.268C11.843 6.139 12.194 6.075 12.557 6.075C12.745 6.075 12.915 6.103 13.07 6.16C13.225 6.217 13.357 6.306 13.466 6.427C13.575 6.548 13.659 6.706 13.718 6.899C13.777 7.092 13.806 7.326 13.806 7.599L11.995 7.851C11.651 7.898 11.355 7.977 11.107 8.088C10.859 8.199 10.654 8.339 10.492 8.507C10.33 8.675 10.21 8.868 10.132 9.087C10.054 9.306 10.015 9.546 10.015 9.808C10.015 10.054 10.057 10.283 10.139 10.496C10.221 10.709 10.342 10.893 10.502 11.047C10.662 11.201 10.862 11.323 11.1 11.413C11.338 11.503 11.613 11.548 11.926 11.548C12.328 11.548 12.686 11.456 13.001 11.27C13.316 11.084 13.573 10.816 13.772 10.464H13.795ZM11.667 8.721C11.843 8.657 12.068 8.607 12.341 8.572L13.806 8.367V8.976C13.806 9.222 13.765 9.451 13.683 9.664C13.601 9.877 13.486 10.063 13.34 10.221C13.194 10.379 13.019 10.503 12.816 10.593C12.613 10.683 12.39 10.728 12.148 10.728C11.961 10.728 11.795 10.703 11.653 10.652C11.511 10.601 11.392 10.53 11.296 10.441C11.2 10.352 11.127 10.247 11.076 10.125C11.025 10.003 11 9.873 11 9.732C11 9.568 11.018 9.421 11.055 9.292C11.092 9.163 11.16 9.051 11.257 8.958C11.354 8.865 11.491 8.785 11.667 8.721Z
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingImage>
</Image>
</ToggleButton>
<ToggleButton Classes="filter-text-box-toggle"
ToolTip.Tip="Match Whole Word"
IsChecked="{Binding $parent[controls:FilterTextBox].UseWholeWordFilter}">
<Image>
<DrawingImage>
<GeometryDrawing Brush="{Binding $parent[ToggleButton].Foreground}">
<GeometryDrawing.Geometry>
M1 2H15V3H1V2ZM14 4H13V12H14V4ZM11.272 8.387C11.194 8.088 11.073 7.825 10.912 7.601C10.751 7.377 10.547 7.2 10.303 7.071C10.059 6.942 9.769 6.878 9.437 6.878C9.239 6.878 9.057 6.902 8.89 6.951C8.725 7 8.574 7.068 8.437 7.156C8.301 7.244 8.18 7.35 8.072 7.474L7.893 7.732V4.578H7V12H7.893V11.425L8.019 11.6C8.106 11.702 8.208 11.79 8.323 11.869C8.44 11.947 8.572 12.009 8.721 12.055C8.87 12.101 9.035 12.123 9.219 12.123C9.572 12.123 9.885 12.052 10.156 11.911C10.428 11.768 10.655 11.573 10.838 11.325C11.021 11.075 11.159 10.782 11.252 10.446C11.345 10.108 11.392 9.743 11.392 9.349C11.391 9.007 11.352 8.686 11.272 8.387ZM9.793 7.78C9.944 7.851 10.075 7.956 10.183 8.094C10.292 8.234 10.377 8.407 10.438 8.611C10.489 8.785 10.52 8.982 10.527 9.198L10.52 9.323C10.52 9.65 10.487 9.943 10.42 10.192C10.353 10.438 10.259 10.645 10.142 10.806C10.025 10.968 9.882 11.091 9.721 11.172C9.399 11.334 8.961 11.338 8.652 11.187C8.499 11.112 8.366 11.012 8.259 10.891C8.174 10.795 8.103 10.675 8.041 10.524C8.041 10.524 7.862 10.077 7.862 9.577C7.862 9.077 8.041 8.575 8.041 8.575C8.103 8.398 8.177 8.257 8.265 8.145C8.379 8.002 8.521 7.886 8.689 7.8C8.857 7.714 9.054 7.671 9.276 7.671C9.466 7.671 9.64 7.708 9.793 7.78ZM15 13H1V14H15V13ZM2.813 10L2.085 12.031H1L1.025 11.959L3.466 4.87305H4.407L6.892 12.031H5.81L5.032 10H2.813ZM3.934 6.42205H3.912L3.007 9.17505H4.848L3.934 6.42205Z
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingImage>
</Image>
</ToggleButton>
<ToggleButton Classes="filter-text-box-toggle"
ToolTip.Tip="Use Regular Expression"
IsChecked="{Binding $parent[controls:FilterTextBox].UseRegexFilter}">
<Image>
<DrawingImage>
<GeometryDrawing Brush="{Binding $parent[ToggleButton].Foreground}">
<GeometryDrawing.Geometry>
M10.0122 2H10.9879V5.11346L13.5489 3.55609L14.034 4.44095L11.4702 6L14.034 7.55905L13.5489 8.44391L10.9879 6.88654V10H10.0122V6.88654L7.45114 8.44391L6.96606 7.55905L9.5299 6L6.96606 4.44095L7.45114 3.55609L10.0122 5.11346V2ZM2 10H6V14H2V10Z
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingImage>
</Image>
</ToggleButton>
</StackPanel>
</Template>
</Setter>
</Style>
<Style Selector="TextBox.filter-text-box Button.textBoxClearButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="TextBox.filter-text-box[AcceptsReturn=False][IsReadOnly=False]:focus:not(TextBox:empty) Button.textBoxClearButton">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="ToggleButton.filter-text-box-toggle > Image">
<Setter Property="Width" Value="16" />
<Setter Property="Height" Value="16" />
<Setter Property="Opacity" Value="0.5" />
</Style>
<Style Selector="ToggleButton.filter-text-box-toggle:pointerover > Image">
<Setter Property="Opacity" Value="0.7" />
</Style>
<Style Selector="ToggleButton.filter-text-box-toggle:pressed > Image, ToggleButton.filter-text-box-toggle:checked > Image">
<Setter Property="Opacity" Value="0.7" />
</Style>
<Style Selector="ToggleButton.filter-text-box-toggle">
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Padding" Value="0,1" />
<Setter Property="Width" Value="24" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="VerticalAlignment" Value="Top" />
</Style>
<Style Selector="ToggleButton.filter-text-box-toggle /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="CornerRadius" Value="0" />
</Style>
<Style Selector="ToggleButton.filter-text-box-toggle:pressed /template/ ContentPresenter, ToggleButton.filter-text-box-toggle:checked /template/ ContentPresenter">
<Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentColor}" />
<Setter Property="Background" Value="{DynamicResource ThemeAccentColor4}" />
</Style>
</Styles>

52
src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs

@ -0,0 +1,52 @@
using System;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Styling;
namespace Avalonia.Diagnostics.Controls
{
internal class FilterTextBox : TextBox, IStyleable
{
public static readonly DirectProperty<FilterTextBox, bool> UseRegexFilterProperty =
AvaloniaProperty.RegisterDirect<FilterTextBox, bool>(nameof(UseRegexFilter),
o => o.UseRegexFilter, (o, v) => o.UseRegexFilter = v,
defaultBindingMode: BindingMode.TwoWay);
public static readonly DirectProperty<FilterTextBox, bool> UseCaseSensitiveFilterProperty =
AvaloniaProperty.RegisterDirect<FilterTextBox, bool>(nameof(UseCaseSensitiveFilter),
o => o.UseCaseSensitiveFilter, (o, v) => o.UseCaseSensitiveFilter = v,
defaultBindingMode: BindingMode.TwoWay);
public static readonly DirectProperty<FilterTextBox, bool> UseWholeWordFilterProperty =
AvaloniaProperty.RegisterDirect<FilterTextBox, bool>(nameof(UseWholeWordFilter),
o => o.UseWholeWordFilter, (o, v) => o.UseWholeWordFilter = v,
defaultBindingMode: BindingMode.TwoWay);
private bool _useRegexFilter, _useCaseSensitiveFilter, _useWholeWordFilter;
public FilterTextBox()
{
Classes.Add("filter-text-box");
}
public bool UseRegexFilter
{
get => _useRegexFilter;
set => SetAndRaise(UseRegexFilterProperty, ref _useRegexFilter, value);
}
public bool UseCaseSensitiveFilter
{
get => _useCaseSensitiveFilter;
set => SetAndRaise(UseCaseSensitiveFilterProperty, ref _useCaseSensitiveFilter, value);
}
public bool UseWholeWordFilter
{
get => _useWholeWordFilter;
set => SetAndRaise(UseWholeWordFilterProperty, ref _useWholeWordFilter, value);
}
Type IStyleable.StyleKey => typeof(TextBox);
}
}

87
src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml

@ -0,0 +1,87 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Avalonia.Diagnostics.Controls">
<Styles.Resources>
<SolidColorBrush x:Key="HighlightBorderBrush" Color="CornflowerBlue" />
<SolidColorBrush x:Key="ThicknessBorderBrush" Color="#666666" />
</Styles.Resources>
<Style Selector="controls|ThicknessEditor">
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="{StaticResource ThicknessBorderBrush}" />
<Setter Property="Template">
<ControlTemplate>
<Panel>
<Rectangle x:Name="PART_Background"
Classes.no-content-pointerover="{Binding !#PART_ContentPresenter.IsPointerOver}" />
<Border
x:Name="PART_Border"
Classes.no-content-pointerover="{Binding !#PART_ContentPresenter.IsPointerOver}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid RowDefinitions="Auto,*,Auto" ColumnDefinitions="Auto,*,Auto">
<Grid.Styles>
<Style Selector="TextBox.thickness-edit">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Margin" Value="2" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="(ScrollViewer.HorizontalScrollBarVisibility)"
Value="Disabled" />
<Setter Property="(ScrollViewer.VerticalScrollBarVisibility)" Value="Disabled" />
<Setter Property="IsVisible"
Value="{Binding $parent[controls:ThicknessEditor].IsPresent}" />
</Style>
</Grid.Styles>
<TextBlock IsVisible="{TemplateBinding IsPresent}" Margin="4,0,0,0"
Text="{TemplateBinding Header}" Grid.Row="0" Grid.Column="0"
Grid.ColumnSpan="2" />
<TextBox Grid.Row="1" Grid.Column="0"
Text="{Binding Left, RelativeSource={RelativeSource TemplatedParent}}"
Classes="thickness-edit" />
<TextBox x:Name="Right" Grid.Row="0" Grid.Column="1"
Text="{Binding Top, RelativeSource={RelativeSource TemplatedParent}}"
Classes="thickness-edit" />
<TextBox Grid.Row="1" Grid.Column="2"
Text="{Binding Right, RelativeSource={RelativeSource TemplatedParent}}"
Classes="thickness-edit" />
<TextBox Grid.Row="2" Grid.Column="1"
Text="{Binding Bottom, RelativeSource={RelativeSource TemplatedParent}}"
Classes="thickness-edit" />
<ContentPresenter Grid.Row="1" Grid.Column="1"
Name="PART_ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" />
</Grid>
</Border>
</Panel>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="controls|ThicknessEditor[IsPresent=False]">
<Setter Property="BorderThickness" Value="0" />
</Style>
<Style Selector="controls|ThicknessEditor /template/ Rectangle#PART_Background">
<Setter Property="Fill" Value="{Binding Background, RelativeSource={RelativeSource TemplatedParent}}" />
</Style>
<Style Selector="controls|ThicknessEditor:pointerover /template/ Rectangle#PART_Background.no-content-pointerover">
<Setter Property="Fill" Value="{Binding Highlight, RelativeSource={RelativeSource TemplatedParent}}" />
</Style>
<Style Selector="controls|ThicknessEditor:pointerover /template/ Border#PART_Border.no-content-pointerover">
<Setter Property="BorderBrush" Value="{StaticResource HighlightBorderBrush}" />
</Style>
</Styles>

33
src/Avalonia.Diagnostics/Diagnostics/Views/ThicknessEditor.cs → src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs

@ -1,29 +1,17 @@
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace Avalonia.Diagnostics.Views
namespace Avalonia.Diagnostics.Controls
{
internal static class Converters
{
public static IValueConverter HasConstraintConverter =
new FuncValueConverter<object, TextDecorationCollection>(ConvertToDecoration);
private static TextDecorationCollection ConvertToDecoration(object arg)
{
return arg != null ? TextDecorations.Underline : null;
}
}
internal class ThicknessEditor : ContentControl
{
public static readonly DirectProperty<ThicknessEditor, Thickness> ThicknessProperty =
AvaloniaProperty.RegisterDirect<ThicknessEditor, Thickness>(nameof(Thickness), o => o.Thickness,
(o, v) => o.Thickness = v, defaultBindingMode: BindingMode.TwoWay);
public static readonly DirectProperty<ThicknessEditor, string> HeaderProperty =
AvaloniaProperty.RegisterDirect<ThicknessEditor, string>(nameof(Header), o => o.Header,
public static readonly DirectProperty<ThicknessEditor, string?> HeaderProperty =
AvaloniaProperty.RegisterDirect<ThicknessEditor, string?>(nameof(Header), o => o.Header,
(o, v) => o.Header = v);
public static readonly DirectProperty<ThicknessEditor, bool> IsPresentProperty =
@ -44,15 +32,16 @@ namespace Avalonia.Diagnostics.Views
AvaloniaProperty.RegisterDirect<ThicknessEditor, double>(nameof(Bottom), o => o.Bottom,
(o, v) => o.Bottom = v);
public static readonly StyledProperty<IBrush> HighlightProperty =
AvaloniaProperty.Register<ThicknessEditor, IBrush>(nameof(Highlight));
private Thickness _thickness;
private string _header;
private string? _header;
private bool _isPresent = true;
private double _left;
private double _top;
private double _right;
private double _bottom;
private bool _isUpdatingThickness;
public Thickness Thickness
@ -61,7 +50,7 @@ namespace Avalonia.Diagnostics.Views
set => SetAndRaise(ThicknessProperty, ref _thickness, value);
}
public string Header
public string? Header
{
get => _header;
set => SetAndRaise(HeaderProperty, ref _header, value);
@ -97,6 +86,12 @@ namespace Avalonia.Diagnostics.Views
set => SetAndRaise(BottomProperty, ref _bottom, value);
}
public IBrush Highlight
{
get => GetValue(HighlightProperty);
set => SetValue(HighlightProperty, value);
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);

22
src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs

@ -1,22 +0,0 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace Avalonia.Diagnostics.Converters
{
internal class BoolToBrushConverter : IValueConverter
{
public IBrush Brush { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? Brush : Brushes.Transparent;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

11
src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs

@ -8,12 +8,17 @@ namespace Avalonia.Diagnostics.Converters
{
public double Opacity { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return (bool)value ? 1d : Opacity;
if (value is bool boolean && boolean)
{
return 1d;
}
return Opacity;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}

25
src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs

@ -0,0 +1,25 @@
using System;
using System.Globalization;
using Avalonia.Data;
using Avalonia.Data.Converters;
namespace Avalonia.Diagnostics.Converters
{
internal class EnumToCheckedConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return Equals(value, parameter);
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is bool isChecked && isChecked)
{
return parameter;
}
return BindingOperations.DoNothing;
}
}
}

14
src/Avalonia.Diagnostics/Diagnostics/DevTools.cs

@ -6,13 +6,12 @@ using Avalonia.Diagnostics.Views;
using Avalonia.Input;
using Avalonia.Interactivity;
#nullable enable
namespace Avalonia.Diagnostics
{
public static class DevTools
{
private static readonly Dictionary<TopLevel, Window> s_open = new Dictionary<TopLevel, Window>();
private static readonly Dictionary<TopLevel, MainWindow> s_open =
new Dictionary<TopLevel, MainWindow>();
public static IDisposable Attach(TopLevel root, KeyGesture gesture)
{
@ -24,7 +23,7 @@ namespace Avalonia.Diagnostics
public static IDisposable Attach(TopLevel root, DevToolsOptions options)
{
void PreviewKeyDown(object sender, KeyEventArgs e)
void PreviewKeyDown(object? sender, KeyEventArgs e)
{
if (options.Gesture.Matches(e))
{
@ -54,6 +53,7 @@ namespace Avalonia.Diagnostics
Width = options.Size.Width,
Height = options.Size.Height,
};
window.SetOptions(options);
window.Closed += DevToolsClosed;
s_open.Add(root, window);
@ -71,10 +71,10 @@ namespace Avalonia.Diagnostics
return Disposable.Create(() => window?.Close());
}
private static void DevToolsClosed(object sender, EventArgs e)
private static void DevToolsClosed(object? sender, EventArgs e)
{
var window = (MainWindow)sender;
s_open.Remove(window.Root);
var window = (MainWindow)sender!;
s_open.Remove(window.Root!);
window.Closed -= DevToolsClosed;
}
}

9
src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs

@ -19,8 +19,13 @@ namespace Avalonia.Diagnostics
public bool ShowAsChildWindow { get; set; } = true;
/// <summary>
/// Gets or sets the initial size of the DevTools window. The default value is 1024x512.
/// Gets or sets the initial size of the DevTools window. The default value is 1280x720.
/// </summary>
public Size Size { get; set; } = new Size(1024, 512);
public Size Size { get; set; } = new Size(1280, 720);
/// <summary>
/// Get or set the startup screen index where the DevTools window will be displayed.
/// </summary>
public int? StartupScreenIndex { get; set; }
}
}

4
src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs

@ -22,8 +22,8 @@ The following commands are available:
clear(): Clear the output history
";
public dynamic e { get; internal set; }
public dynamic root { get; internal set; }
public dynamic? e { get; internal set; }
public dynamic? root { get; internal set; }
internal static object NoOutput { get; } = new object();

6
src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs

@ -7,15 +7,15 @@ namespace Avalonia.Diagnostics.Models
{
public EventChainLink(object handler, bool handled, RoutingStrategies route)
{
Contract.Requires<ArgumentNullException>(handler != null);
Handler = handler;
Handler = handler ?? throw new ArgumentNullException(nameof(handler));
Handled = handled;
Route = route;
}
public object Handler { get; }
public bool BeginsNewRoute { get; set; }
public string HandlerName
{
get

4
src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs

@ -9,12 +9,12 @@ namespace Avalonia.Diagnostics
{
public IControl Build(object data)
{
var name = data.GetType().FullName.Replace("ViewModel", "View");
var name = data.GetType().FullName!.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type);
return (Control)Activator.CreateInstance(type)!;
}
else
{

49
src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs

@ -1,17 +1,17 @@
using System.ComponentModel;
using Avalonia.Collections;
namespace Avalonia.Diagnostics.ViewModels
{
internal class AvaloniaPropertyViewModel : PropertyViewModel
{
private readonly AvaloniaObject _target;
private string _type;
private object _value;
private object? _value;
private string _priority;
private string _group;
#nullable disable
// Remove "nullable disable" after MemberNotNull will work on our CI.
public AvaloniaPropertyViewModel(AvaloniaObject o, AvaloniaProperty property)
#nullable restore
{
_target = o;
Property = property;
@ -20,25 +20,17 @@ namespace Avalonia.Diagnostics.ViewModels
$"[{property.OwnerType.Name}.{property.Name}]" :
property.Name;
if (property.IsDirect)
{
_group = "Properties";
Priority = "Direct";
}
Update();
}
public AvaloniaProperty Property { get; }
public override object Key => Property;
public override string Name { get; }
public bool IsAttached => Property.IsAttached;
public override bool? IsAttached =>
Property.IsAttached;
public string Priority
{
get => _priority;
private set => RaiseAndSetIfChanged(ref _priority, value);
}
public override string Priority =>
_priority;
public override string Type => _type;
@ -56,40 +48,37 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public override string Group
{
get => _group;
}
public override string Group => _group;
// [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))]
public override void Update()
{
if (Property.IsDirect)
{
RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value));
RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type));
RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type));
RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority));
_group = "Properties";
}
else
{
var val = _target.GetDiagnostic(Property);
RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value));
RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type));
RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type));
if (val != null)
{
SetGroup(IsAttached ? "Attached Properties" : "Properties");
Priority = val.Priority.ToString();
RaiseAndSetIfChanged(ref _priority, val.Priority.ToString(), nameof(Priority));
RaiseAndSetIfChanged(ref _group, IsAttached == true ? "Attached Properties" : "Properties", nameof(Group));
}
else
{
SetGroup(Priority = "Unset");
RaiseAndSetIfChanged(ref _priority, "Unset", nameof(Priority));
RaiseAndSetIfChanged(ref _group, "Unset", nameof(Group));
}
}
}
private void SetGroup(string group)
{
RaiseAndSetIfChanged(ref _group, group, nameof(Group));
}
}
}

19
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs

@ -1,5 +1,4 @@
using System.ComponentModel;
using System.Reflection;
using System.Reflection;
namespace Avalonia.Diagnostics.ViewModels
{
@ -7,14 +6,17 @@ namespace Avalonia.Diagnostics.ViewModels
{
private readonly object _target;
private string _type;
private object _value;
private object? _value;
#nullable disable
// Remove "nullable disable" after MemberNotNull will work on our CI.
public ClrPropertyViewModel(object o, PropertyInfo property)
#nullable restore
{
_target = o;
Property = property;
if (!property.DeclaringType.IsInterface)
if (property.DeclaringType == null || !property.DeclaringType.IsInterface)
{
Name = property.Name;
}
@ -47,11 +49,18 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public override string Priority =>
string.Empty;
public override bool? IsAttached =>
default;
// [MemberNotNull(nameof(_type))]
public override void Update()
{
var val = Property.GetValue(_target);
RaiseAndSetIfChanged(ref _value, val, nameof(Value));
RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type));
RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type));
}
}
}

3
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs

@ -15,11 +15,12 @@ namespace Avalonia.Diagnostics.ViewModels
private int _historyIndex = -1;
private string _input;
private bool _isVisible;
private ScriptState<object> _state;
private ScriptState<object>? _state;
public ConsoleViewModel(Action<ConsoleContext> updateContext)
{
_context = new ConsoleContext(this);
_input = string.Empty;
_updateContext = updateContext;
}

68
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@ -18,11 +18,10 @@ namespace Avalonia.Diagnostics.ViewModels
{
private readonly IVisual _control;
private readonly IDictionary<object, List<PropertyViewModel>> _propertyIndex;
private AvaloniaPropertyViewModel _selectedProperty;
private string _styleFilter;
private PropertyViewModel? _selectedProperty;
private bool _snapshotStyles;
private bool _showInactiveStyles;
private string _styleStatus;
private string? _styleStatus;
public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control)
{
@ -84,7 +83,8 @@ namespace Avalonia.Diagnostics.ViewModels
{
foreach (var setter in style.Setters)
{
if (setter is Setter regularSetter)
if (setter is Setter regularSetter
&& regularSetter.Property != null)
{
var setterValue = regularSetter.Value;
@ -116,13 +116,14 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
private (object resourceKey, bool isDynamic)? GetResourceInfo(object value)
private (object resourceKey, bool isDynamic)? GetResourceInfo(object? value)
{
if (value is StaticResourceExtension staticResource)
{
return (staticResource.ResourceKey, false);
}
else if (value is DynamicResourceExtension dynamicResource)
else if (value is DynamicResourceExtension dynamicResource
&& dynamicResource.ResourceKey != null)
{
return (dynamicResource.ResourceKey, true);
}
@ -138,18 +139,12 @@ namespace Avalonia.Diagnostics.ViewModels
public ObservableCollection<PseudoClassViewModel> PseudoClasses { get; }
public AvaloniaPropertyViewModel SelectedProperty
public PropertyViewModel? SelectedProperty
{
get => _selectedProperty;
set => RaiseAndSetIfChanged(ref _selectedProperty, value);
}
public string StyleFilter
{
get => _styleFilter;
set => RaiseAndSetIfChanged(ref _styleFilter, value);
}
public bool SnapshotStyles
{
get => _snapshotStyles;
@ -162,7 +157,7 @@ namespace Avalonia.Diagnostics.ViewModels
set => RaiseAndSetIfChanged(ref _showInactiveStyles, value);
}
public string StyleStatus
public string? StyleStatus
{
get => _styleStatus;
set => RaiseAndSetIfChanged(ref _styleStatus, value);
@ -174,11 +169,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
base.OnPropertyChanged(e);
if (e.PropertyName == nameof(StyleFilter))
{
UpdateStyleFilters();
}
else if (e.PropertyName == nameof(SnapshotStyles))
if (e.PropertyName == nameof(SnapshotStyles))
{
if (!SnapshotStyles)
{
@ -187,19 +178,15 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
private void UpdateStyleFilters()
public void UpdateStyleFilters()
{
var filter = StyleFilter;
bool hasFilter = !string.IsNullOrEmpty(filter);
foreach (var style in AppliedStyles)
{
var hasVisibleSetter = false;
foreach (var setter in style.Setters)
{
setter.IsVisible =
!hasFilter || setter.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
setter.IsVisible = TreePage.SettersFilter.Filter(setter.Name);
hasVisibleSetter |= setter.IsVisible;
}
@ -263,7 +250,7 @@ namespace Avalonia.Diagnostics.ViewModels
.Select(x => new ClrPropertyViewModel(o, x));
}
private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
private void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (_propertyIndex.TryGetValue(e.Property, out var properties))
{
@ -276,9 +263,10 @@ namespace Avalonia.Diagnostics.ViewModels
Layout.ControlPropertyChanged(sender, e);
}
private void ControlPropertyChanged(object sender, PropertyChangedEventArgs e)
private void ControlPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (_propertyIndex.TryGetValue(e.PropertyName, out var properties))
if (e.PropertyName != null
&& _propertyIndex.TryGetValue(e.PropertyName, out var properties))
{
foreach (var property in properties)
{
@ -292,7 +280,7 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
private void OnClassesChanged(object sender, NotifyCollectionChangedEventArgs e)
private void OnClassesChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (!SnapshotStyles)
{
@ -357,27 +345,17 @@ namespace Avalonia.Diagnostics.ViewModels
private bool FilterProperty(object arg)
{
if (!string.IsNullOrWhiteSpace(TreePage.PropertyFilter) && arg is PropertyViewModel property)
{
if (TreePage.UseRegexFilter)
{
return TreePage.FilterRegex?.IsMatch(property.Name) ?? true;
}
return property.Name.IndexOf(TreePage.PropertyFilter, StringComparison.OrdinalIgnoreCase) != -1;
}
return true;
return !(arg is PropertyViewModel property) || TreePage.PropertiesFilter.Filter(property.Name);
}
private class PropertyComparer : IComparer<PropertyViewModel>
{
public static PropertyComparer Instance { get; } = new PropertyComparer();
public int Compare(PropertyViewModel x, PropertyViewModel y)
public int Compare(PropertyViewModel? x, PropertyViewModel? y)
{
var groupX = GroupIndex(x.Group);
var groupY = GroupIndex(y.Group);
var groupX = GroupIndex(x?.Group);
var groupY = GroupIndex(y?.Group);
if (groupX != groupY)
{
@ -385,11 +363,11 @@ namespace Avalonia.Diagnostics.ViewModels
}
else
{
return string.CompareOrdinal(x.Name, y.Name);
return string.CompareOrdinal(x?.Name, y?.Name);
}
}
private int GroupIndex(string group)
private int GroupIndex(string? group)
{
switch (group)
{

140
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs

@ -1,4 +1,6 @@
using System;
using System.ComponentModel;
using System.Text;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.VisualTree;
@ -8,27 +10,58 @@ namespace Avalonia.Diagnostics.ViewModels
internal class ControlLayoutViewModel : ViewModelBase
{
private readonly IVisual _control;
private Thickness _marginThickness;
private Thickness _borderThickness;
private Thickness _paddingThickness;
private double _width;
private double _height;
private string _widthConstraint;
private string _heightConstraint;
private string? _heightConstraint;
private HorizontalAlignment _horizontalAlignment;
private Thickness _marginThickness;
private Thickness _paddingThickness;
private bool _updatingFromControl;
private VerticalAlignment _verticalAlignment;
private double _width;
private string? _widthConstraint;
public ControlLayoutViewModel(IVisual control)
{
_control = control;
HasPadding = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Decorator.PaddingProperty);
HasBorder = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Border.BorderThicknessProperty);
if (control is AvaloniaObject ao)
{
MarginThickness = ao.GetValue(Layoutable.MarginProperty);
if (HasPadding)
{
PaddingThickness = ao.GetValue(Decorator.PaddingProperty);
}
if (HasBorder)
{
BorderThickness = ao.GetValue(Border.BorderThicknessProperty);
}
HorizontalAlignment = ao.GetValue(Layoutable.HorizontalAlignmentProperty);
VerticalAlignment = ao.GetValue(Layoutable.VerticalAlignmentProperty);
}
UpdateSize();
UpdateSizeConstraints();
}
public Thickness MarginThickness
{
get => _marginThickness;
set => RaiseAndSetIfChanged(ref _marginThickness, value);
}
public Thickness BorderThickness
{
get => _borderThickness;
set => RaiseAndSetIfChanged(ref _borderThickness, value);
}
public Thickness PaddingThickness
{
get => _paddingThickness;
@ -47,60 +80,61 @@ namespace Avalonia.Diagnostics.ViewModels
private set => RaiseAndSetIfChanged(ref _height, value);
}
public string WidthConstraint
public string? WidthConstraint
{
get => _widthConstraint;
private set => RaiseAndSetIfChanged(ref _widthConstraint, value);
}
public string HeightConstraint
public string? HeightConstraint
{
get => _heightConstraint;
private set => RaiseAndSetIfChanged(ref _heightConstraint, value);
}
public bool HasPadding { get; }
public bool HasBorder { get; }
public ControlLayoutViewModel(IVisual control)
public HorizontalAlignment HorizontalAlignment
{
_control = control;
HasPadding = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Decorator.PaddingProperty);
HasBorder = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Border.BorderThicknessProperty);
if (control is AvaloniaObject ao)
{
MarginThickness = ao.GetValue(Layoutable.MarginProperty);
get => _horizontalAlignment;
private set => RaiseAndSetIfChanged(ref _horizontalAlignment, value);
}
if (HasPadding)
{
PaddingThickness = ao.GetValue(Decorator.PaddingProperty);
}
public VerticalAlignment VerticalAlignment
{
get => _verticalAlignment;
private set => RaiseAndSetIfChanged(ref _verticalAlignment, value);
}
if (HasBorder)
{
BorderThickness = ao.GetValue(Border.BorderThicknessProperty);
}
}
public bool HasPadding { get; }
UpdateSize();
UpdateSizeConstraints();
}
public bool HasBorder { get; }
private void UpdateSizeConstraints()
{
if (_control is IAvaloniaObject ao)
{
string CreateConstraintInfo(StyledProperty<double> minProperty, StyledProperty<double> maxProperty)
string? CreateConstraintInfo(StyledProperty<double> minProperty, StyledProperty<double> maxProperty)
{
if (ao.IsSet(minProperty) || ao.IsSet(maxProperty))
bool hasMin = ao.IsSet(minProperty);
bool hasMax = ao.IsSet(maxProperty);
if (hasMin || hasMax)
{
var minValue = ao.GetValue(minProperty);
var maxValue = ao.GetValue(maxProperty);
var builder = new StringBuilder();
if (hasMin)
{
var minValue = ao.GetValue(minProperty);
builder.AppendFormat("Min: {0}", Math.Round(minValue, 2));
builder.AppendLine();
}
return $"{minValue} < size < {maxValue}";
if (hasMax)
{
var maxValue = ao.GetValue(maxProperty);
builder.AppendFormat("Max: {0}", Math.Round(maxValue, 2));
}
return builder.ToString();
}
return null;
@ -134,10 +168,18 @@ namespace Avalonia.Diagnostics.ViewModels
{
ao.SetValue(Border.BorderThicknessProperty, BorderThickness);
}
else if (e.PropertyName == nameof(HorizontalAlignment))
{
ao.SetValue(Layoutable.HorizontalAlignmentProperty, HorizontalAlignment);
}
else if (e.PropertyName == nameof(VerticalAlignment))
{
ao.SetValue(Layoutable.VerticalAlignmentProperty, VerticalAlignment);
}
}
}
public void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
public void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
try
{
@ -154,15 +196,15 @@ namespace Avalonia.Diagnostics.ViewModels
if (e.Property == Layoutable.MarginProperty)
{
MarginThickness = ao.GetValue(Layoutable.MarginProperty);
}
}
else if (e.Property == Decorator.PaddingProperty)
{
PaddingThickness = ao.GetValue(Decorator.PaddingProperty);
}
}
else if (e.Property == Border.BorderThicknessProperty)
{
BorderThickness = ao.GetValue(Border.BorderThicknessProperty);
}
}
else if (e.Property == Layoutable.MinWidthProperty ||
e.Property == Layoutable.MaxWidthProperty ||
e.Property == Layoutable.MinHeightProperty ||
@ -170,6 +212,14 @@ namespace Avalonia.Diagnostics.ViewModels
{
UpdateSizeConstraints();
}
else if (e.Property == Layoutable.HorizontalAlignmentProperty)
{
HorizontalAlignment = ao.GetValue(Layoutable.HorizontalAlignmentProperty);
}
else if (e.Property == Layoutable.VerticalAlignmentProperty)
{
VerticalAlignment = ao.GetValue(Layoutable.VerticalAlignmentProperty);
}
}
}
}
@ -183,8 +233,8 @@ namespace Avalonia.Diagnostics.ViewModels
{
var size = _control.Bounds;
Width = size.Width;
Height = size.Height;
Width = Math.Round(size.Width, 2);
Height = Math.Round(size.Height, 2);
}
}
}

12
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs

@ -2,25 +2,17 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace Avalonia.Diagnostics.ViewModels
{
internal class EventOwnerTreeNode : EventTreeNodeBase
{
private static readonly RoutedEvent[] s_defaultEvents =
{
Button.ClickEvent, InputElement.KeyDownEvent, InputElement.KeyUpEvent, InputElement.TextInputEvent,
InputElement.PointerReleasedEvent, InputElement.PointerPressedEvent
};
public EventOwnerTreeNode(Type type, IEnumerable<RoutedEvent> events, EventsPageViewModel vm)
: base(null, type.Name)
{
Children = new AvaloniaList<EventTreeNodeBase>(events.OrderBy(e => e.Name)
.Select(e => new EventTreeNode(this, e, vm) { IsEnabled = s_defaultEvents.Contains(e) }));
.Select(e => new EventTreeNode(this, e, vm)));
IsExpanded = true;
}
@ -35,7 +27,7 @@ namespace Avalonia.Diagnostics.ViewModels
if (_updateChildren && value != null)
{
foreach (var child in Children)
foreach (var child in Children!)
{
try
{

30
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs

@ -9,21 +9,19 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class EventTreeNode : EventTreeNodeBase
{
private readonly RoutedEvent _event;
private readonly EventsPageViewModel _parentViewModel;
private bool _isRegistered;
private FiredEvent _currentEvent;
private FiredEvent? _currentEvent;
public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsPageViewModel vm)
: base(parent, @event.Name)
{
Contract.Requires<ArgumentNullException>(@event != null);
Contract.Requires<ArgumentNullException>(vm != null);
_event = @event;
_parentViewModel = vm;
Event = @event ?? throw new ArgumentNullException(nameof(@event));
_parentViewModel = vm ?? throw new ArgumentNullException(nameof(vm));
}
public RoutedEvent Event { get; }
public override bool? IsEnabled
{
get => base.IsEnabled;
@ -53,24 +51,26 @@ namespace Avalonia.Diagnostics.ViewModels
{
if (IsEnabled.GetValueOrDefault() && !_isRegistered)
{
var allRoutes = RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble;
// FIXME: This leaks event handlers.
_event.AddClassHandler(typeof(object), HandleEvent, (RoutingStrategies)7, handledEventsToo: true);
Event.AddClassHandler(typeof(object), HandleEvent, allRoutes, handledEventsToo: true);
_isRegistered = true;
}
}
private void HandleEvent(object sender, RoutedEventArgs e)
private void HandleEvent(object? sender, RoutedEventArgs e)
{
if (!_isRegistered || IsEnabled == false)
return;
if (sender is IVisual v && BelongsToDevTool(v))
return;
var s = sender;
var s = sender!;
var handled = e.Handled;
var route = e.Route;
Action handler = delegate
void handler()
{
if (_currentEvent == null || !_currentEvent.IsPartOfSameEventChain(e))
{
@ -95,14 +95,16 @@ namespace Avalonia.Diagnostics.ViewModels
private static bool BelongsToDevTool(IVisual v)
{
while (v != null)
var current = v;
while (current != null)
{
if (v is MainView || v is MainWindow)
if (current is MainView || current is MainWindow)
{
return true;
}
v = v.VisualParent;
current = current.VisualParent;
}
return false;

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

Loading…
Cancel
Save