Browse Source

Merge branch 'master' into fixed-property-equals-selector

pull/5150/head
Dariusz Komosiński 5 years ago
committed by GitHub
parent
commit
525373e8fe
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      native/Avalonia.Native/src/OSX/window.h
  2. 31
      native/Avalonia.Native/src/OSX/window.mm
  3. 4
      readme.md
  4. 6
      samples/ControlCatalog/Pages/ContextFlyoutPage.axaml
  5. 36
      samples/RenderDemo/Pages/TransitionsPage.xaml
  6. 21
      src/Avalonia.Animation/AnimationInstance`1.cs
  7. 20
      src/Avalonia.Animation/AnimatorDrivenTransition.cs
  8. 32
      src/Avalonia.Animation/AnimatorTransitionObservable.cs
  9. 9
      src/Avalonia.Animation/Clock.cs
  10. 12
      src/Avalonia.Animation/ClockBase.cs
  11. 6
      src/Avalonia.Animation/Transition.cs
  12. 60
      src/Avalonia.Animation/TransitionInstance.cs
  13. 58
      src/Avalonia.Animation/TransitionObservableBase.cs
  14. 13
      src/Avalonia.Animation/Transitions/DoubleTransition.cs
  15. 13
      src/Avalonia.Animation/Transitions/FloatTransition.cs
  16. 13
      src/Avalonia.Animation/Transitions/IntegerTransition.cs
  17. 39
      src/Avalonia.Base/AvaloniaObject.cs
  18. 2
      src/Avalonia.Base/Data/BindingValue.cs
  19. 2
      src/Avalonia.Base/Data/Core/StreamNode.cs
  20. 4
      src/Avalonia.Base/Diagnostics/IAvaloniaObjectDebug.cs
  21. 9
      src/Avalonia.Base/IAvaloniaObject.cs
  22. 81
      src/Avalonia.Base/Metadata/NullableAttributes.cs
  23. 2
      src/Avalonia.Controls/Control.cs
  24. 4
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  25. 4
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  26. 3
      src/Avalonia.Controls/ExperimentalAcrylicBorder.cs
  27. 4
      src/Avalonia.Controls/IControl.cs
  28. 22
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  29. 3
      src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs
  30. 26
      src/Avalonia.Controls/Shapes/Shape.cs
  31. 112
      src/Avalonia.Controls/TextBox.cs
  32. 3
      src/Avalonia.Controls/TickBar.cs
  33. 5
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  34. 11
      src/Avalonia.Controls/Utils/UndoRedoHelper.cs
  35. 12
      src/Avalonia.Controls/Window.cs
  36. 4
      src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
  37. 103
      src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml
  38. 52
      src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs
  39. 8
      src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs
  40. 11
      src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs
  41. 4
      src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs
  42. 10
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  43. 4
      src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs
  44. 4
      src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
  45. 4
      src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs
  46. 49
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
  47. 19
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
  48. 3
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs
  49. 68
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  50. 12
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs
  51. 2
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs
  52. 23
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
  53. 6
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs
  54. 36
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs
  55. 122
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs
  56. 15
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
  57. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs
  58. 23
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  59. 12
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
  60. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs
  61. 10
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
  62. 16
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  63. 39
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs
  64. 110
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  65. 12
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs
  66. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
  67. 14
      src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs
  68. 36
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  69. 9
      src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
  70. 2
      src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs
  71. 8
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs
  72. 1
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
  73. 33
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  74. 24
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
  75. 2
      src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs
  76. 2
      src/Avalonia.Input/IInputElement.cs
  77. 2
      src/Avalonia.Input/InputElement.cs
  78. 4
      src/Avalonia.Input/KeyboardDevice.cs
  79. 8
      src/Avalonia.Input/MouseDevice.cs
  80. 2
      src/Avalonia.Interactivity/IInteractive.cs
  81. 2
      src/Avalonia.Interactivity/Interactive.cs
  82. 2
      src/Avalonia.Layout/Layoutable.cs
  83. 6
      src/Avalonia.Styling/Controls/ISetInheritanceParent.cs
  84. 6
      src/Avalonia.Styling/Controls/ISetLogicalParent.cs
  85. 6
      src/Avalonia.Styling/IDataContextProvider.cs
  86. 6
      src/Avalonia.Styling/IStyledElement.cs
  87. 4
      src/Avalonia.Styling/StyledElement.cs
  88. 4
      src/Avalonia.Styling/Styling/ISetterValue.cs
  89. 4
      src/Avalonia.Styling/Styling/PropertySetterInstance.cs
  90. 4
      src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs
  91. 7
      src/Avalonia.Themes.Default/TextBox.xaml
  92. 13
      src/Avalonia.Visuals/Animation/Transitions/BoxShadowsTransition.cs
  93. 59
      src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs
  94. 12
      src/Avalonia.Visuals/Animation/Transitions/ColorTransition.cs
  95. 13
      src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs
  96. 13
      src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs
  97. 13
      src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs
  98. 13
      src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs
  99. 18
      src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs
  100. 13
      src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs

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

@ -34,7 +34,6 @@ class WindowBaseImpl;
-(double) getScaling; -(double) getScaling;
-(double) getExtendedTitleBarHeight; -(double) getExtendedTitleBarHeight;
-(void) setIsExtended:(bool)value; -(void) setIsExtended:(bool)value;
-(void) updateShadow;
@end @end
struct INSWindowHolder struct INSWindowHolder

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

@ -50,7 +50,6 @@ public:
[Window setBackingType:NSBackingStoreBuffered]; [Window setBackingType:NSBackingStoreBuffered];
[Window setOpaque:false]; [Window setOpaque:false];
[Window setContentView: StandardContainer];
} }
virtual HRESULT ObtainNSWindowHandle(void** ret) override virtual HRESULT ObtainNSWindowHandle(void** ret) override
@ -112,6 +111,9 @@ public:
{ {
SetPosition(lastPositionSet); SetPosition(lastPositionSet);
UpdateStyle(); UpdateStyle();
[Window setContentView: StandardContainer];
if(ShouldTakeFocusOnShow() && activate) if(ShouldTakeFocusOnShow() && activate)
{ {
[Window makeKeyAndOrderFront:Window]; [Window makeKeyAndOrderFront:Window];
@ -125,10 +127,6 @@ public:
_shown = true; _shown = true;
dispatch_async(dispatch_get_main_queue(), ^{
[Window updateShadow];
});
return S_OK; return S_OK;
} }
} }
@ -195,9 +193,11 @@ public:
{ {
if(ret == nullptr) if(ret == nullptr)
return E_POINTER; return E_POINTER;
auto frame = [View frame]; auto frame = [View frame];
ret->Width = frame.size.width; ret->Width = frame.size.width;
ret->Height = frame.size.height; ret->Height = frame.size.height;
return S_OK; return S_OK;
} }
} }
@ -258,6 +258,12 @@ public:
y = maxSize.height; y = maxSize.height;
} }
if(!_shown)
{
BaseEvents->Resized(AvnSize{x,y});
}
[StandardContainer setFrameSize:NSSize{x,y}];
[Window setContentSize:NSSize{x, y}]; [Window setContentSize:NSSize{x, y}];
return S_OK; return S_OK;
@ -1842,19 +1848,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
double _lastScaling; double _lastScaling;
} }
- (void)updateShadow
{
// Common problem in Cocoa where [invalidateShadow] does work,
// This hack forces Cocoa to invalidate the shadow.
NSRect frame = [self frame];
NSRect updatedFrame = NSMakeRect(frame.origin.x, frame.origin.y, frame.size.width + 1.0, frame.size.height + 1.0);
[self setFrame:updatedFrame display:YES];
[self setFrame:frame display:YES];
[self invalidateShadow];
}
-(void) setIsExtended:(bool)value; -(void) setIsExtended:(bool)value;
{ {
_isExtended = value; _isExtended = value;
@ -2013,7 +2006,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_lastScaling = [self backingScaleFactor]; _lastScaling = [self backingScaleFactor];
[self setOpaque:NO]; [self setOpaque:NO];
[self setBackgroundColor: [NSColor clearColor]]; [self setBackgroundColor: [NSColor clearColor]];
[self invalidateShadow];
_isExtended = false; _isExtended = false;
return self; return self;
} }
@ -2254,6 +2246,7 @@ protected:
{ {
if (Window != nullptr) if (Window != nullptr)
{ {
[StandardContainer setFrameSize:NSSize{x,y}];
[Window setContentSize:NSSize{x, y}]; [Window setContentSize:NSSize{x, y}];
[Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))]; [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 ## 🚀 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/ 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
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 ## Building and Using

6
samples/ControlCatalog/Pages/ContextFlyoutPage.axaml

@ -84,13 +84,13 @@
<Button Name="CutButton" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}"> <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" /> <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>
<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" /> <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>
<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" /> <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>
<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" /> <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> </Button>
</StackPanel> </StackPanel>

36
samples/RenderDemo/Pages/TransitionsPage.xaml

@ -141,6 +141,39 @@
<Style Selector="Border.Shadow:pointerover"> <Style Selector="Border.Shadow:pointerover">
<Setter Property="BoxShadow" Value="inset 30 30 20 30 Green, 20 40 20 10 Red"/> <Setter Property="BoxShadow" Value="inset 30 30 20 30 Green, 20 40 20 10 Red"/>
</Style> </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> </Styles>
</UserControl.Styles> </UserControl.Styles>
@ -166,6 +199,9 @@
<Border Classes="Test Shadow" CornerRadius="10" Child="{x:Null}" /> <Border Classes="Test Shadow" CornerRadius="10" Child="{x:Null}" />
<Border Classes="Test Shadow" CornerRadius="0 30 60 0" Child="{x:Null}" /> <Border Classes="Test Shadow" CornerRadius="0 30 60 0" Child="{x:Null}" />
<Border Classes="Test Rect10" />
<Border Classes="Test Rect11" />
</WrapPanel> </WrapPanel>
</StackPanel> </StackPanel>
</Grid> </Grid>

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

@ -5,6 +5,7 @@ using Avalonia.Animation.Animators;
using Avalonia.Animation.Utils; using Avalonia.Animation.Utils;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Reactive; using Avalonia.Reactive;
using JetBrains.Annotations;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
@ -45,8 +46,9 @@ namespace Avalonia.Animation
_onCompleteAction = OnComplete; _onCompleteAction = OnComplete;
_interpolator = Interpolator; _interpolator = Interpolator;
_baseClock = baseClock; _baseClock = baseClock;
_neutralValue = (T)_targetControl.GetValue(_animator.Property); control.PropertyChanged += ControlPropertyChanged;
UpdateNeutralValue();
FetchProperties(); FetchProperties();
} }
@ -216,5 +218,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);
}
}
}

9
src/Avalonia.Animation/Clock.cs

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

12
src/Avalonia.Animation/ClockBase.cs

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

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

@ -1,7 +1,5 @@
using System; using System;
using System.Reactive.Linq;
using Avalonia.Animation.Easings; using Avalonia.Animation.Easings;
using Avalonia.Animation.Utils;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
@ -56,4 +54,4 @@ namespace Avalonia.Animation
return control.Bind<T>((AvaloniaProperty<T>)Property, transition, Data.BindingPriority.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;
using System.Reactive.Linq; using System.Runtime.ExceptionServices;
using Avalonia.Animation.Easings;
using Avalonia.Animation.Utils;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -11,13 +8,13 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Handles the timing and lifetime of a <see cref="Transition{T}"/>. /// Handles the timing and lifetime of a <see cref="Transition{T}"/>.
/// </summary> /// </summary>
internal class TransitionInstance : SingleSubscriberObservableBase<double> internal class TransitionInstance : SingleSubscriberObservableBase<double>, IObserver<TimeSpan>
{ {
private IDisposable _timerSubscription; private IDisposable _timerSubscription;
private TimeSpan _delay; private TimeSpan _delay;
private TimeSpan _duration; private TimeSpan _duration;
private readonly IClock _baseClock; private readonly IClock _baseClock;
private IClock _clock; private TransitionClock _clock;
public TransitionInstance(IClock clock, TimeSpan delay, TimeSpan duration) public TransitionInstance(IClock clock, TimeSpan delay, TimeSpan duration)
{ {
@ -75,9 +72,56 @@ namespace Avalonia.Animation
protected override void Subscribed() protected override void Subscribed()
{ {
_clock = new Clock(_baseClock); _clock = new TransitionClock(_baseClock);
_timerSubscription = _clock.Subscribe(TimerTick); _timerSubscription = _clock.Subscribe(this);
PublishNext(0.0d); 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));
}
}
}

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

@ -1,6 +1,3 @@
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
namespace Avalonia.Animation namespace Avalonia.Animation
@ -8,15 +5,7 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="double"/> types. /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="double"/> types.
/// </summary> /// </summary>
public class DoubleTransition : Transition<double> public class DoubleTransition : AnimatorDrivenTransition<double, DoubleAnimator>
{ {
private static readonly DoubleAnimator s_animator = new DoubleAnimator();
/// <inheritdocs/>
public override IObservable<double> DoTransition(IObservable<double> progress, double oldValue, double newValue)
{
return progress
.Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
} }
} }

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

@ -1,6 +1,3 @@
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
namespace Avalonia.Animation namespace Avalonia.Animation
@ -8,15 +5,7 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="float"/> types. /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="float"/> types.
/// </summary> /// </summary>
public class FloatTransition : Transition<float> public class FloatTransition : AnimatorDrivenTransition<float, FloatAnimator>
{ {
private static readonly FloatAnimator s_animator = new FloatAnimator();
/// <inheritdocs/>
public override IObservable<float> DoTransition(IObservable<double> progress, float oldValue, float newValue)
{
return progress
.Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
} }
} }

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

@ -1,6 +1,3 @@
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
namespace Avalonia.Animation namespace Avalonia.Animation
@ -8,15 +5,7 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="int"/> types. /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="int"/> types.
/// </summary> /// </summary>
public class IntegerTransition : Transition<int> public class IntegerTransition : AnimatorDrivenTransition<int, Int32Animator>
{ {
private static readonly Int32Animator s_animator = new Int32Animator();
/// <inheritdocs/>
public override IObservable<int> DoTransition(IObservable<double> progress, int oldValue, int newValue)
{
return progress
.Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
} }
} }

39
src/Avalonia.Base/AvaloniaObject.cs

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

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

@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -358,6 +359,7 @@ namespace Avalonia.Data
e); e);
} }
[Conditional("DEBUG")]
private static void ValidateValue([AllowNull] T value) private static void ValidateValue([AllowNull] T value)
{ {
if (value is UnsetValueType) 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) protected override void StartListeningCore(WeakReference<object> reference)
{ {
GetPlugin(reference)?.Start(reference).Subscribe(ValueChanged); _subscription = GetPlugin(reference)?.Start(reference).Subscribe(ValueChanged);
} }
protected override void StopListeningCore() protected override void StopListeningCore()

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

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

9
src/Avalonia.Base/IAvaloniaObject.cs

@ -1,6 +1,8 @@
using System; using System;
using Avalonia.Data; using Avalonia.Data;
#nullable enable
namespace Avalonia namespace Avalonia
{ {
/// <summary> /// <summary>
@ -11,7 +13,7 @@ namespace Avalonia
/// <summary> /// <summary>
/// Raised when a <see cref="AvaloniaProperty"/> value changes on this object. /// Raised when a <see cref="AvaloniaProperty"/> value changes on this object.
/// </summary> /// </summary>
event EventHandler<AvaloniaPropertyChangedEventArgs> PropertyChanged; event EventHandler<AvaloniaPropertyChangedEventArgs>? PropertyChanged;
/// <summary> /// <summary>
/// Clears an <see cref="AvaloniaProperty"/>'s local value. /// Clears an <see cref="AvaloniaProperty"/>'s local value.
@ -75,7 +77,10 @@ namespace Avalonia
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
/// <param name="priority">The priority of 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, StyledPropertyBase<T> property,
T value, T value,
BindingPriority priority = BindingPriority.LocalValue); 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 #pragma warning disable MA0048 // File name must match type name
#define INTERNAL_NULLABLE_ATTRIBUTES #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 // https://github.com/dotnet/corefx/blob/48363ac826ccf66fbe31a5dcb1dc2aab9a7dd768/src/Common/src/CoreLib/System/Diagnostics/CodeAnalysis/NullableAttributes.cs
@ -10,6 +9,7 @@
namespace System.Diagnostics.CodeAnalysis 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> /// <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)] [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
#if INTERNAL_NULLABLE_ATTRIBUTES #if INTERNAL_NULLABLE_ATTRIBUTES
@ -136,5 +136,82 @@ namespace System.Diagnostics.CodeAnalysis
/// <summary>Gets the condition parameter value.</summary> /// <summary>Gets the condition parameter value.</summary>
public bool ParameterValue { get; } 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 #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.Controls/Control.cs

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

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

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

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

@ -2,6 +2,7 @@
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes; using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data;
using System; using System;
using System.Globalization; using System.Globalization;
@ -44,7 +45,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public static readonly DirectProperty<TimePicker, TimeSpan?> SelectedTimeProperty = public static readonly DirectProperty<TimePicker, TimeSpan?> SelectedTimeProperty =
AvaloniaProperty.RegisterDirect<TimePicker, TimeSpan?>(nameof(SelectedTime), 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 // Template Items
private TimePickerPresenter _presenter; private TimePickerPresenter _presenter;

3
src/Avalonia.Controls/ExperimentalAcrylicBorder.cs

@ -3,6 +3,7 @@ using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using System; using System;
using Avalonia.Media.Immutable;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -90,7 +91,7 @@ namespace Avalonia.Controls
} }
else 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);
} }
} }

4
src/Avalonia.Controls/IControl.cs

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

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

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

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

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

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

@ -1,6 +1,7 @@
using System; using System;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Immutable;
#nullable enable #nullable enable
@ -199,8 +200,29 @@ namespace Avalonia.Controls.Shapes
if (geometry != null) if (geometry != null)
{ {
var pen = new Pen(Stroke, StrokeThickness, new DashStyle(StrokeDashArray, StrokeDashOffset), var stroke = Stroke;
StrokeLineCap, StrokeJoin);
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); context.DrawGeometry(Fill, pen, geometry);
} }
} }

112
src/Avalonia.Controls/TextBox.cs

@ -119,9 +119,9 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<TextBox, bool>(nameof(RevealPassword)); AvaloniaProperty.Register<TextBox, bool>(nameof(RevealPassword));
public static readonly DirectProperty<TextBox, bool> CanCutProperty = public static readonly DirectProperty<TextBox, bool> CanCutProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>( AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanCut), nameof(CanCut),
o => o.CanCut); o => o.CanCut);
public static readonly DirectProperty<TextBox, bool> CanCopyProperty = public static readonly DirectProperty<TextBox, bool> CanCopyProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>( AvaloniaProperty.RegisterDirect<TextBox, bool>(
@ -129,9 +129,21 @@ namespace Avalonia.Controls
o => o.CanCopy); o => o.CanCopy);
public static readonly DirectProperty<TextBox, bool> CanPasteProperty = public static readonly DirectProperty<TextBox, bool> CanPasteProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>( AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanPaste), nameof(CanPaste),
o => o.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> struct UndoRedoState : IEquatable<UndoRedoState>
{ {
@ -218,7 +230,7 @@ namespace Avalonia.Controls
value = CoerceCaretIndex(value); value = CoerceCaretIndex(value);
SetAndRaise(CaretIndexProperty, ref _caretIndex, value); SetAndRaise(CaretIndexProperty, ref _caretIndex, value);
UndoRedoState state; UndoRedoState state;
if (_undoRedoHelper.TryGetLastState(out state) && state.Text == Text) if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text)
_undoRedoHelper.UpdateLastState(); _undoRedoHelper.UpdateLastState();
} }
} }
@ -316,7 +328,7 @@ namespace Avalonia.Controls
SelectionEnd = CoerceCaretIndex(SelectionEnd, value); SelectionEnd = CoerceCaretIndex(SelectionEnd, value);
CaretIndex = CoerceCaretIndex(caretIndex, value); CaretIndex = CoerceCaretIndex(caretIndex, value);
if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing) if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing)
{ {
_undoRedoHelper.Clear(); _undoRedoHelper.Clear();
} }
@ -329,7 +341,7 @@ namespace Avalonia.Controls
get { return GetSelection(); } get { return GetSelection(); }
set set
{ {
_undoRedoHelper.Snapshot(); SnapshotUndoRedo();
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
{ {
DeleteSelection(); DeleteSelection();
@ -338,7 +350,7 @@ namespace Avalonia.Controls
{ {
HandleTextInput(value); HandleTextInput(value);
} }
_undoRedoHelper.Snapshot(); SnapshotUndoRedo();
} }
} }
@ -446,6 +458,36 @@ namespace Avalonia.Controls
private set { SetAndRaise(CanPasteProperty, ref _canPaste, value); } 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) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter"); _presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
@ -465,6 +507,15 @@ namespace Avalonia.Controls
UpdatePseudoclasses(); UpdatePseudoclasses();
UpdateCommandStates(); 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() private void UpdateCommandStates()
@ -551,7 +602,10 @@ namespace Avalonia.Controls
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
CaretIndex += input.Length; CaretIndex += input.Length;
ClearSelection(); ClearSelection();
_undoRedoHelper.DiscardRedo(); if (IsUndoEnabled)
{
_undoRedoHelper.DiscardRedo();
}
} }
} }
@ -570,10 +624,10 @@ namespace Avalonia.Controls
var text = GetSelection(); var text = GetSelection();
if (text is null) return; if (text is null) return;
_undoRedoHelper.Snapshot(); SnapshotUndoRedo();
Copy(); Copy();
DeleteSelection(); DeleteSelection();
_undoRedoHelper.Snapshot(); SnapshotUndoRedo();
} }
public async void Copy() public async void Copy()
@ -591,9 +645,9 @@ namespace Avalonia.Controls
if (text is null) return; if (text is null) return;
_undoRedoHelper.Snapshot(); SnapshotUndoRedo();
HandleTextInput(text); HandleTextInput(text);
_undoRedoHelper.Snapshot(); SnapshotUndoRedo();
} }
protected override void OnKeyDown(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e)
@ -638,7 +692,7 @@ namespace Avalonia.Controls
Paste(); Paste();
handled = true; handled = true;
} }
else if (Match(keymap.Undo)) else if (Match(keymap.Undo) && IsUndoEnabled)
{ {
try try
{ {
@ -652,7 +706,7 @@ namespace Avalonia.Controls
handled = true; handled = true;
} }
else if (Match(keymap.Redo)) else if (Match(keymap.Redo) && IsUndoEnabled)
{ {
try try
{ {
@ -752,7 +806,7 @@ namespace Avalonia.Controls
break; break;
case Key.Back: case Key.Back:
_undoRedoHelper.Snapshot(); SnapshotUndoRedo();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd) if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{ {
SetSelectionForControlBackspace(); SetSelectionForControlBackspace();
@ -776,13 +830,13 @@ namespace Avalonia.Controls
CaretIndex -= removedCharacters; CaretIndex -= removedCharacters;
ClearSelection(); ClearSelection();
} }
_undoRedoHelper.Snapshot(); SnapshotUndoRedo();
handled = true; handled = true;
break; break;
case Key.Delete: case Key.Delete:
_undoRedoHelper.Snapshot(); SnapshotUndoRedo();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd) if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{ {
SetSelectionForControlDelete(); SetSelectionForControlDelete();
@ -804,7 +858,7 @@ namespace Avalonia.Controls
SetTextInternal(text.Substring(0, caretIndex) + SetTextInternal(text.Substring(0, caretIndex) +
text.Substring(caretIndex + removedCharacters)); text.Substring(caretIndex + removedCharacters));
} }
_undoRedoHelper.Snapshot(); SnapshotUndoRedo();
handled = true; handled = true;
break; break;
@ -812,9 +866,9 @@ namespace Avalonia.Controls
case Key.Enter: case Key.Enter:
if (AcceptsReturn) if (AcceptsReturn)
{ {
_undoRedoHelper.Snapshot(); SnapshotUndoRedo();
HandleTextInput(NewLine); HandleTextInput(NewLine);
_undoRedoHelper.Snapshot(); SnapshotUndoRedo();
handled = true; handled = true;
} }
@ -823,9 +877,9 @@ namespace Avalonia.Controls
case Key.Tab: case Key.Tab:
if (AcceptsTab) if (AcceptsTab)
{ {
_undoRedoHelper.Snapshot(); SnapshotUndoRedo();
HandleTextInput("\t"); HandleTextInput("\t");
_undoRedoHelper.Snapshot(); SnapshotUndoRedo();
handled = true; handled = true;
} }
else else
@ -1251,5 +1305,13 @@ namespace Avalonia.Controls
ClearSelection(); ClearSelection();
} }
} }
private void SnapshotUndoRedo()
{
if (IsUndoEnabled)
{
_undoRedoHelper.Snapshot();
}
}
} }
} }

3
src/Avalonia.Controls/TickBar.cs

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

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

@ -1,5 +1,6 @@
using System; using System;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -114,9 +115,9 @@ namespace Avalonia.Controls.Utils
var borderThickness = _borderThickness.Top; var borderThickness = _borderThickness.Top;
IPen pen = null; 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); var rect = new Rect(_size);

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

@ -22,6 +22,10 @@ namespace Avalonia.Controls.Utils
private LinkedListNode<TState> _currentNode; 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 int Limit { get; set; } = 10;
public UndoRedoHelper(IUndoRedoHost host) public UndoRedoHelper(IUndoRedoHost host)
@ -54,7 +58,10 @@ namespace Avalonia.Controls.Utils
public bool HasState => _currentNode != null; public bool HasState => _currentNode != null;
public void UpdateLastState(TState state) public void UpdateLastState(TState state)
{ {
_states.Last.Value = state; if (_states.Last != null)
{
_states.Last.Value = state;
}
} }
public void UpdateLastState() public void UpdateLastState()
@ -86,7 +93,7 @@ namespace Avalonia.Controls.Utils
DiscardRedo(); DiscardRedo();
_states.AddLast(current); _states.AddLast(current);
_currentNode = _states.Last; _currentNode = _states.Last;
if (_states.Count > Limit) if (Limit != -1 && _states.Count > Limit)
_states.RemoveFirst(); _states.RemoveFirst();
} }
} }

12
src/Avalonia.Controls/Window.cs

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

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

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

8
src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs

@ -10,8 +10,8 @@ namespace Avalonia.Diagnostics.Controls
AvaloniaProperty.RegisterDirect<ThicknessEditor, Thickness>(nameof(Thickness), o => o.Thickness, AvaloniaProperty.RegisterDirect<ThicknessEditor, Thickness>(nameof(Thickness), o => o.Thickness,
(o, v) => o.Thickness = v, defaultBindingMode: BindingMode.TwoWay); (o, v) => o.Thickness = v, defaultBindingMode: BindingMode.TwoWay);
public static readonly DirectProperty<ThicknessEditor, string> HeaderProperty = public static readonly DirectProperty<ThicknessEditor, string?> HeaderProperty =
AvaloniaProperty.RegisterDirect<ThicknessEditor, string>(nameof(Header), o => o.Header, AvaloniaProperty.RegisterDirect<ThicknessEditor, string?>(nameof(Header), o => o.Header,
(o, v) => o.Header = v); (o, v) => o.Header = v);
public static readonly DirectProperty<ThicknessEditor, bool> IsPresentProperty = public static readonly DirectProperty<ThicknessEditor, bool> IsPresentProperty =
@ -36,7 +36,7 @@ namespace Avalonia.Diagnostics.Controls
AvaloniaProperty.Register<ThicknessEditor, IBrush>(nameof(Highlight)); AvaloniaProperty.Register<ThicknessEditor, IBrush>(nameof(Highlight));
private Thickness _thickness; private Thickness _thickness;
private string _header; private string? _header;
private bool _isPresent = true; private bool _isPresent = true;
private double _left; private double _left;
private double _top; private double _top;
@ -50,7 +50,7 @@ namespace Avalonia.Diagnostics.Controls
set => SetAndRaise(ThicknessProperty, ref _thickness, value); set => SetAndRaise(ThicknessProperty, ref _thickness, value);
} }
public string Header public string? Header
{ {
get => _header; get => _header;
set => SetAndRaise(HeaderProperty, ref _header, value); set => SetAndRaise(HeaderProperty, ref _header, value);

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

@ -8,12 +8,17 @@ namespace Avalonia.Diagnostics.Converters
{ {
public double Opacity { get; set; } 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(); throw new NotImplementedException();
} }

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

@ -7,12 +7,12 @@ namespace Avalonia.Diagnostics.Converters
{ {
internal class EnumToCheckedConverter : IValueConverter internal class EnumToCheckedConverter : IValueConverter
{ {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{ {
return Equals(value, parameter); return Equals(value, parameter);
} }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{ {
if (value is bool isChecked && isChecked) if (value is bool isChecked && isChecked)
{ {

10
src/Avalonia.Diagnostics/Diagnostics/DevTools.cs

@ -6,8 +6,6 @@ using Avalonia.Diagnostics.Views;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
#nullable enable
namespace Avalonia.Diagnostics namespace Avalonia.Diagnostics
{ {
public static class DevTools public static class DevTools
@ -24,7 +22,7 @@ namespace Avalonia.Diagnostics
public static IDisposable Attach(TopLevel root, DevToolsOptions options) 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)) if (options.Gesture.Matches(e))
{ {
@ -71,10 +69,10 @@ namespace Avalonia.Diagnostics
return Disposable.Create(() => window?.Close()); 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; var window = (MainWindow)sender!;
s_open.Remove(window.Root); s_open.Remove(window.Root!);
window.Closed -= DevToolsClosed; window.Closed -= DevToolsClosed;
} }
} }

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

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

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

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

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

@ -9,12 +9,12 @@ namespace Avalonia.Diagnostics
{ {
public IControl Build(object data) 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); var type = Type.GetType(name);
if (type != null) if (type != null)
{ {
return (Control)Activator.CreateInstance(type); return (Control)Activator.CreateInstance(type)!;
} }
else else
{ {

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

@ -1,17 +1,17 @@
using System.ComponentModel;
using Avalonia.Collections;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
internal class AvaloniaPropertyViewModel : PropertyViewModel internal class AvaloniaPropertyViewModel : PropertyViewModel
{ {
private readonly AvaloniaObject _target; private readonly AvaloniaObject _target;
private string _type; private string _type;
private object _value; private object? _value;
private string _priority; private string _priority;
private string _group; private string _group;
#nullable disable
// Remove "nullable disable" after MemberNotNull will work on our CI.
public AvaloniaPropertyViewModel(AvaloniaObject o, AvaloniaProperty property) public AvaloniaPropertyViewModel(AvaloniaObject o, AvaloniaProperty property)
#nullable restore
{ {
_target = o; _target = o;
Property = property; Property = property;
@ -20,25 +20,17 @@ namespace Avalonia.Diagnostics.ViewModels
$"[{property.OwnerType.Name}.{property.Name}]" : $"[{property.OwnerType.Name}.{property.Name}]" :
property.Name; property.Name;
if (property.IsDirect)
{
_group = "Properties";
Priority = "Direct";
}
Update(); Update();
} }
public AvaloniaProperty Property { get; } public AvaloniaProperty Property { get; }
public override object Key => Property; public override object Key => Property;
public override string Name { get; } public override string Name { get; }
public bool IsAttached => Property.IsAttached; public override bool? IsAttached =>
Property.IsAttached;
public string Priority public override string Priority =>
{ _priority;
get => _priority;
private set => RaiseAndSetIfChanged(ref _priority, value);
}
public override string Type => _type; public override string Type => _type;
@ -56,40 +48,37 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
public override string Group public override string Group => _group;
{
get => _group;
}
// [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))]
public override void Update() public override void Update()
{ {
if (Property.IsDirect) if (Property.IsDirect)
{ {
RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value)); 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 else
{ {
var val = _target.GetDiagnostic(Property); var val = _target.GetDiagnostic(Property);
RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value)); 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) if (val != null)
{ {
SetGroup(IsAttached ? "Attached Properties" : "Properties"); RaiseAndSetIfChanged(ref _priority, val.Priority.ToString(), nameof(Priority));
Priority = val.Priority.ToString(); RaiseAndSetIfChanged(ref _group, IsAttached == true ? "Attached Properties" : "Properties", nameof(Group));
} }
else 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 namespace Avalonia.Diagnostics.ViewModels
{ {
@ -7,14 +6,17 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
private readonly object _target; private readonly object _target;
private string _type; 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) public ClrPropertyViewModel(object o, PropertyInfo property)
#nullable restore
{ {
_target = o; _target = o;
Property = property; Property = property;
if (!property.DeclaringType.IsInterface) if (property.DeclaringType == null || !property.DeclaringType.IsInterface)
{ {
Name = property.Name; 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() public override void Update()
{ {
var val = Property.GetValue(_target); var val = Property.GetValue(_target);
RaiseAndSetIfChanged(ref _value, val, nameof(Value)); 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 int _historyIndex = -1;
private string _input; private string _input;
private bool _isVisible; private bool _isVisible;
private ScriptState<object> _state; private ScriptState<object>? _state;
public ConsoleViewModel(Action<ConsoleContext> updateContext) public ConsoleViewModel(Action<ConsoleContext> updateContext)
{ {
_context = new ConsoleContext(this); _context = new ConsoleContext(this);
_input = string.Empty;
_updateContext = updateContext; _updateContext = updateContext;
} }

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

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

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

@ -12,14 +12,14 @@ namespace Avalonia.Diagnostics.ViewModels
private readonly IVisual _control; private readonly IVisual _control;
private Thickness _borderThickness; private Thickness _borderThickness;
private double _height; private double _height;
private string _heightConstraint; private string? _heightConstraint;
private HorizontalAlignment _horizontalAlignment; private HorizontalAlignment _horizontalAlignment;
private Thickness _marginThickness; private Thickness _marginThickness;
private Thickness _paddingThickness; private Thickness _paddingThickness;
private bool _updatingFromControl; private bool _updatingFromControl;
private VerticalAlignment _verticalAlignment; private VerticalAlignment _verticalAlignment;
private double _width; private double _width;
private string _widthConstraint; private string? _widthConstraint;
public ControlLayoutViewModel(IVisual control) public ControlLayoutViewModel(IVisual control)
{ {
@ -80,13 +80,13 @@ namespace Avalonia.Diagnostics.ViewModels
private set => RaiseAndSetIfChanged(ref _height, value); private set => RaiseAndSetIfChanged(ref _height, value);
} }
public string WidthConstraint public string? WidthConstraint
{ {
get => _widthConstraint; get => _widthConstraint;
private set => RaiseAndSetIfChanged(ref _widthConstraint, value); private set => RaiseAndSetIfChanged(ref _widthConstraint, value);
} }
public string HeightConstraint public string? HeightConstraint
{ {
get => _heightConstraint; get => _heightConstraint;
private set => RaiseAndSetIfChanged(ref _heightConstraint, value); private set => RaiseAndSetIfChanged(ref _heightConstraint, value);
@ -112,7 +112,7 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
if (_control is IAvaloniaObject ao) if (_control is IAvaloniaObject ao)
{ {
string CreateConstraintInfo(StyledProperty<double> minProperty, StyledProperty<double> maxProperty) string? CreateConstraintInfo(StyledProperty<double> minProperty, StyledProperty<double> maxProperty)
{ {
bool hasMin = ao.IsSet(minProperty); bool hasMin = ao.IsSet(minProperty);
bool hasMax = ao.IsSet(maxProperty); bool hasMax = ao.IsSet(maxProperty);
@ -179,7 +179,7 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
public void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) public void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{ {
try try
{ {

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

@ -27,7 +27,7 @@ namespace Avalonia.Diagnostics.ViewModels
if (_updateChildren && value != null) if (_updateChildren && value != null)
{ {
foreach (var child in Children) foreach (var child in Children!)
{ {
try try
{ {

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

@ -11,16 +11,13 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
private readonly EventsPageViewModel _parentViewModel; private readonly EventsPageViewModel _parentViewModel;
private bool _isRegistered; private bool _isRegistered;
private FiredEvent _currentEvent; private FiredEvent? _currentEvent;
public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsPageViewModel vm) public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsPageViewModel vm)
: base(parent, @event.Name) : base(parent, @event.Name)
{ {
Contract.Requires<ArgumentNullException>(@event != null); Event = @event ?? throw new ArgumentNullException(nameof(@event));
Contract.Requires<ArgumentNullException>(vm != null); _parentViewModel = vm ?? throw new ArgumentNullException(nameof(vm));
Event = @event;
_parentViewModel = vm;
} }
public RoutedEvent Event { get; } public RoutedEvent Event { get; }
@ -62,18 +59,18 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
private void HandleEvent(object sender, RoutedEventArgs e) private void HandleEvent(object? sender, RoutedEventArgs e)
{ {
if (!_isRegistered || IsEnabled == false) if (!_isRegistered || IsEnabled == false)
return; return;
if (sender is IVisual v && BelongsToDevTool(v)) if (sender is IVisual v && BelongsToDevTool(v))
return; return;
var s = sender; var s = sender!;
var handled = e.Handled; var handled = e.Handled;
var route = e.Route; var route = e.Route;
Action handler = delegate void handler()
{ {
if (_currentEvent == null || !_currentEvent.IsPartOfSameEventChain(e)) if (_currentEvent == null || !_currentEvent.IsPartOfSameEventChain(e))
{ {
@ -98,14 +95,16 @@ namespace Avalonia.Diagnostics.ViewModels
private static bool BelongsToDevTool(IVisual v) 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; return true;
} }
v = v.VisualParent; current = current.VisualParent;
} }
return false; return false;

6
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs

@ -10,14 +10,14 @@ namespace Avalonia.Diagnostics.ViewModels
private bool? _isEnabled = false; private bool? _isEnabled = false;
private bool _isVisible; private bool _isVisible;
protected EventTreeNodeBase(EventTreeNodeBase parent, string text) protected EventTreeNodeBase(EventTreeNodeBase? parent, string text)
{ {
Parent = parent; Parent = parent;
Text = text; Text = text;
IsVisible = true; IsVisible = true;
} }
public IAvaloniaReadOnlyList<EventTreeNodeBase> Children public IAvaloniaReadOnlyList<EventTreeNodeBase>? Children
{ {
get; get;
protected set; protected set;
@ -41,7 +41,7 @@ namespace Avalonia.Diagnostics.ViewModels
set => RaiseAndSetIfChanged(ref _isVisible, value); set => RaiseAndSetIfChanged(ref _isVisible, value);
} }
public EventTreeNodeBase Parent public EventTreeNodeBase? Parent
{ {
get; get;
} }

36
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq; using System.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Diagnostics.Models; using Avalonia.Diagnostics.Models;
@ -23,9 +22,8 @@ namespace Avalonia.Diagnostics.ViewModels
}; };
private readonly MainViewModel _mainViewModel; private readonly MainViewModel _mainViewModel;
private string _eventTypeFilter; private FiredEvent? _selectedEvent;
private FiredEvent _selectedEvent; private EventTreeNodeBase? _selectedNode;
private EventTreeNodeBase _selectedNode;
public EventsPageViewModel(MainViewModel mainViewModel) public EventsPageViewModel(MainViewModel mainViewModel)
{ {
@ -37,6 +35,9 @@ namespace Avalonia.Diagnostics.ViewModels
.Select(g => new EventOwnerTreeNode(g.Key, g, this)) .Select(g => new EventOwnerTreeNode(g.Key, g, this))
.ToArray(); .ToArray();
EventsFilter = new FilterViewModel();
EventsFilter.RefreshFilter += (s, e) => UpdateEventFilters();
EnableDefault(); EnableDefault();
} }
@ -46,23 +47,19 @@ namespace Avalonia.Diagnostics.ViewModels
public ObservableCollection<FiredEvent> RecordedEvents { get; } = new ObservableCollection<FiredEvent>(); public ObservableCollection<FiredEvent> RecordedEvents { get; } = new ObservableCollection<FiredEvent>();
public FiredEvent SelectedEvent public FiredEvent? SelectedEvent
{ {
get => _selectedEvent; get => _selectedEvent;
set => RaiseAndSetIfChanged(ref _selectedEvent, value); set => RaiseAndSetIfChanged(ref _selectedEvent, value);
} }
public EventTreeNodeBase SelectedNode public EventTreeNodeBase? SelectedNode
{ {
get => _selectedNode; get => _selectedNode;
set => RaiseAndSetIfChanged(ref _selectedNode, value); set => RaiseAndSetIfChanged(ref _selectedNode, value);
} }
public string EventTypeFilter public FilterViewModel EventsFilter { get; }
{
get => _eventTypeFilter;
set => RaiseAndSetIfChanged(ref _eventTypeFilter, value);
}
public void Clear() public void Clear()
{ {
@ -101,7 +98,7 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
static EventTreeNodeBase FindNode(EventTreeNodeBase node, RoutedEvent eventType) static EventTreeNodeBase? FindNode(EventTreeNodeBase node, RoutedEvent eventType)
{ {
if (node is EventTreeNode eventNode && eventNode.Event == eventType) if (node is EventTreeNode eventNode && eventNode.Event == eventType)
{ {
@ -125,16 +122,6 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.PropertyName == nameof(EventTypeFilter))
{
UpdateEventFilters();
}
}
private void EvaluateNodeEnabled(Func<EventTreeNode, bool> eval) private void EvaluateNodeEnabled(Func<EventTreeNode, bool> eval)
{ {
void ProcessNode(EventTreeNodeBase node) void ProcessNode(EventTreeNodeBase node)
@ -161,9 +148,6 @@ namespace Avalonia.Diagnostics.ViewModels
private void UpdateEventFilters() private void UpdateEventFilters()
{ {
var filter = EventTypeFilter;
bool hasFilter = !string.IsNullOrEmpty(filter);
foreach (var node in Nodes) foreach (var node in Nodes)
{ {
FilterNode(node, false); FilterNode(node, false);
@ -171,7 +155,7 @@ namespace Avalonia.Diagnostics.ViewModels
bool FilterNode(EventTreeNodeBase node, bool isParentVisible) bool FilterNode(EventTreeNodeBase node, bool isParentVisible)
{ {
bool matchesFilter = !hasFilter || node.Text.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0; bool matchesFilter = EventsFilter.Filter(node.Text);
bool hasVisibleChild = false; bool hasVisibleChild = false;
if (node.Children != null) if (node.Children != null)

122
src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs

@ -0,0 +1,122 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text.RegularExpressions;
namespace Avalonia.Diagnostics.ViewModels
{
internal class FilterViewModel : ViewModelBase, INotifyDataErrorInfo
{
private readonly Dictionary<string, string> _errors = new Dictionary<string, string>();
private string _filterString = string.Empty;
private bool _useRegexFilter, _useCaseSensitiveFilter, _useWholeWordFilter;
private Regex? _filterRegex;
public event EventHandler? RefreshFilter;
public bool HasErrors => _errors.Count > 0;
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
public bool Filter(string input)
{
return _filterRegex?.IsMatch(input) ?? true;
}
private void UpdateFilterRegex()
{
void ClearError()
{
if (_errors.Remove(nameof(FilterString)))
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(FilterString)));
}
}
try
{
var options = RegexOptions.Compiled;
var pattern = UseRegexFilter
? FilterString.Trim() : Regex.Escape(FilterString.Trim());
if (!UseCaseSensitiveFilter)
{
options |= RegexOptions.IgnoreCase;
}
if (UseWholeWordFilter)
{
pattern = $"\\b(?:{pattern})\\b";
}
_filterRegex = new Regex(pattern, options);
ClearError();
}
catch (Exception exception)
{
_errors[nameof(FilterString)] = exception.Message;
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(FilterString)));
}
}
public string FilterString
{
get => _filterString;
set
{
if (RaiseAndSetIfChanged(ref _filterString, value))
{
UpdateFilterRegex();
RefreshFilter?.Invoke(this, EventArgs.Empty);
}
}
}
public bool UseRegexFilter
{
get => _useRegexFilter;
set
{
if (RaiseAndSetIfChanged(ref _useRegexFilter, value))
{
UpdateFilterRegex();
RefreshFilter?.Invoke(this, EventArgs.Empty);
}
}
}
public bool UseCaseSensitiveFilter
{
get => _useCaseSensitiveFilter;
set
{
if (RaiseAndSetIfChanged(ref _useCaseSensitiveFilter, value))
{
UpdateFilterRegex();
RefreshFilter?.Invoke(this, EventArgs.Empty);
}
}
}
public bool UseWholeWordFilter
{
get => _useWholeWordFilter;
set
{
if (RaiseAndSetIfChanged(ref _useWholeWordFilter, value))
{
UpdateFilterRegex();
RefreshFilter?.Invoke(this, EventArgs.Empty);
}
}
}
public IEnumerable GetErrors(string? propertyName)
{
if (propertyName != null
&& _errors.TryGetValue(propertyName, out var error))
{
yield return error;
}
}
}
}

15
src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs

@ -8,15 +8,12 @@ namespace Avalonia.Diagnostics.ViewModels
internal class FiredEvent : ViewModelBase internal class FiredEvent : ViewModelBase
{ {
private readonly RoutedEventArgs _eventArgs; private readonly RoutedEventArgs _eventArgs;
private EventChainLink _handledBy; private EventChainLink? _handledBy;
public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator) public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator)
{ {
Contract.Requires<ArgumentNullException>(eventArgs != null); _eventArgs = eventArgs ?? throw new ArgumentNullException(nameof(eventArgs));
Contract.Requires<ArgumentNullException>(originator != null); Originator = originator ?? throw new ArgumentNullException(nameof(originator));
_eventArgs = eventArgs;
Originator = originator;
AddToChain(originator); AddToChain(originator);
} }
@ -25,7 +22,7 @@ namespace Avalonia.Diagnostics.ViewModels
return e == _eventArgs; return e == _eventArgs;
} }
public RoutedEvent Event => _eventArgs.RoutedEvent; public RoutedEvent Event => _eventArgs.RoutedEvent!;
public bool IsHandled => HandledBy?.Handled == true; public bool IsHandled => HandledBy?.Handled == true;
@ -38,7 +35,7 @@ namespace Avalonia.Diagnostics.ViewModels
if (IsHandled) if (IsHandled)
{ {
return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine + return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine +
$"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}"; $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy!.HandlerName}";
} }
return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}"; return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}";
@ -47,7 +44,7 @@ namespace Avalonia.Diagnostics.ViewModels
public EventChainLink Originator { get; } public EventChainLink Originator { get; }
public EventChainLink HandledBy public EventChainLink? HandledBy
{ {
get => _handledBy; get => _handledBy;
set set

8
src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs

@ -7,22 +7,24 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
internal class LogicalTreeNode : TreeNode internal class LogicalTreeNode : TreeNode
{ {
public LogicalTreeNode(ILogical logical, TreeNode parent) public LogicalTreeNode(ILogical logical, TreeNode? parent)
: base((Control)logical, parent) : base((Control)logical, parent)
{ {
Children = new LogicalTreeNodeCollection(this, logical); Children = new LogicalTreeNodeCollection(this, logical);
} }
public override TreeNodeCollection Children { get; }
public static LogicalTreeNode[] Create(object control) public static LogicalTreeNode[] Create(object control)
{ {
var logical = control as ILogical; var logical = control as ILogical;
return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null; return logical != null ? new[] { new LogicalTreeNode(logical, null) } : Array.Empty<LogicalTreeNode>();
} }
internal class LogicalTreeNodeCollection : TreeNodeCollection internal class LogicalTreeNodeCollection : TreeNodeCollection
{ {
private readonly ILogical _control; private readonly ILogical _control;
private IDisposable _subscription; private IDisposable? _subscription;
public LogicalTreeNodeCollection(TreeNode owner, ILogical control) public LogicalTreeNodeCollection(TreeNode owner, ILogical control)
: base(owner) : base(owner)

23
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@ -1,10 +1,10 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Diagnostics.Models; using Avalonia.Diagnostics.Models;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
@ -17,13 +17,16 @@ namespace Avalonia.Diagnostics.ViewModels
private readonly IDisposable _pointerOverSubscription; private readonly IDisposable _pointerOverSubscription;
private ViewModelBase _content; private ViewModelBase _content;
private int _selectedTab; private int _selectedTab;
private string _focusedControl; private string? _focusedControl;
private string _pointerOverElement; private string? _pointerOverElement;
private bool _shouldVisualizeMarginPadding = true; private bool _shouldVisualizeMarginPadding = true;
private bool _shouldVisualizeDirtyRects; private bool _shouldVisualizeDirtyRects;
private bool _showFpsOverlay; private bool _showFpsOverlay;
#nullable disable
// Remove "nullable disable" after MemberNotNull will work on our CI.
public MainViewModel(TopLevel root) public MainViewModel(TopLevel root)
#nullable restore
{ {
_root = root; _root = root;
_logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root)); _logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root));
@ -84,6 +87,7 @@ namespace Avalonia.Diagnostics.ViewModels
public ViewModelBase Content public ViewModelBase Content
{ {
get { return _content; } get { return _content; }
// [MemberNotNull(nameof(_content))]
private set private set
{ {
if (_content is TreePageViewModel oldTree && if (_content is TreePageViewModel oldTree &&
@ -114,34 +118,35 @@ namespace Avalonia.Diagnostics.ViewModels
public int SelectedTab public int SelectedTab
{ {
get { return _selectedTab; } get { return _selectedTab; }
// [MemberNotNull(nameof(_content))]
set set
{ {
_selectedTab = value; _selectedTab = value;
switch (value) switch (value)
{ {
case 0:
Content = _logicalTree;
break;
case 1: case 1:
Content = _visualTree; Content = _visualTree;
break; break;
case 2: case 2:
Content = _events; Content = _events;
break; break;
default:
Content = _logicalTree;
break;
} }
RaisePropertyChanged(); RaisePropertyChanged();
} }
} }
public string FocusedControl public string? FocusedControl
{ {
get { return _focusedControl; } get { return _focusedControl; }
private set { RaiseAndSetIfChanged(ref _focusedControl, value); } private set { RaiseAndSetIfChanged(ref _focusedControl, value); }
} }
public string PointerOverElement public string? PointerOverElement
{ {
get { return _pointerOverElement; } get { return _pointerOverElement; }
private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); } private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); }
@ -187,7 +192,7 @@ namespace Avalonia.Diagnostics.ViewModels
FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name; FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name;
} }
private void KeyboardPropertyChanged(object sender, PropertyChangedEventArgs e) private void KeyboardPropertyChanged(object? sender, PropertyChangedEventArgs e)
{ {
if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement))
{ {

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

@ -16,9 +16,11 @@ namespace Avalonia.Diagnostics.ViewModels
public abstract string Group { get; } public abstract string Group { get; }
public abstract string Type { get; } public abstract string Type { get; }
public abstract string Value { get; set; } public abstract string Value { get; set; }
public abstract void Update(); public abstract string Priority { get; }
public abstract bool? IsAttached { get; }
public abstract void Update();
protected static string ConvertToString(object value) protected static string ConvertToString(object? value)
{ {
if (value is null) if (value is null)
{ {
@ -31,13 +33,13 @@ namespace Avalonia.Diagnostics.ViewModels
if (!converter.CanConvertTo(typeof(string)) || if (!converter.CanConvertTo(typeof(string)) ||
converter.GetType() == typeof(CollectionConverter)) converter.GetType() == typeof(CollectionConverter))
{ {
return value.ToString(); return value.ToString() ?? "(null)";
} }
return converter.ConvertToString(value); return converter.ConvertToString(value);
} }
private static object InvokeParse(string s, Type targetType) private static object? InvokeParse(string s, Type targetType)
{ {
var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null); var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null);
@ -56,7 +58,7 @@ namespace Avalonia.Diagnostics.ViewModels
throw new InvalidCastException("Unable to convert value."); throw new InvalidCastException("Unable to convert value.");
} }
protected static object ConvertFromString(string s, Type targetType) protected static object? ConvertFromString(string s, Type targetType)
{ {
var converter = TypeDescriptor.GetConverter(targetType); var converter = TypeDescriptor.GetConverter(targetType);

8
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs

@ -8,7 +8,7 @@ namespace Avalonia.Diagnostics.ViewModels
public IBrush Tint { get; } public IBrush Tint { get; }
public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object resourceValue, bool isDynamic) : base(property, resourceValue) public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object? resourceValue, bool isDynamic) : base(property, resourceValue)
{ {
Key = resourceKey; Key = resourceKey;
Tint = isDynamic ? Brushes.Orange : Brushes.Brown; Tint = isDynamic ? Brushes.Orange : Brushes.Brown;
@ -16,12 +16,14 @@ namespace Avalonia.Diagnostics.ViewModels
public void CopyResourceKey() public void CopyResourceKey()
{ {
if (Key is null) var textToCopy = Key?.ToString();
if (textToCopy is null)
{ {
return; return;
} }
CopyToClipboard(Key.ToString()); CopyToClipboard(textToCopy);
} }
} }
} }

10
src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs

@ -11,7 +11,7 @@ namespace Avalonia.Diagnostics.ViewModels
public string Name { get; } public string Name { get; }
public object Value { get; } public object? Value { get; }
public bool IsActive public bool IsActive
{ {
@ -25,7 +25,7 @@ namespace Avalonia.Diagnostics.ViewModels
set => RaiseAndSetIfChanged(ref _isVisible, value); set => RaiseAndSetIfChanged(ref _isVisible, value);
} }
public SetterViewModel(AvaloniaProperty property, object value) public SetterViewModel(AvaloniaProperty property, object? value)
{ {
Property = property; Property = property;
Name = property.Name; Name = property.Name;
@ -36,12 +36,14 @@ namespace Avalonia.Diagnostics.ViewModels
public void CopyValue() public void CopyValue()
{ {
if (Value is null) var textToCopy = Value?.ToString();
if (textToCopy is null)
{ {
return; return;
} }
CopyToClipboard(Value.ToString()); CopyToClipboard(textToCopy);
} }
public void CopyPropertyName() public void CopyPropertyName()

16
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs

@ -9,17 +9,18 @@ using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
internal class TreeNode : ViewModelBase, IDisposable internal abstract class TreeNode : ViewModelBase, IDisposable
{ {
private IDisposable _classesSubscription; private IDisposable? _classesSubscription;
private string _classes; private string _classes;
private bool _isExpanded; private bool _isExpanded;
public TreeNode(IVisual visual, TreeNode parent) public TreeNode(IVisual visual, TreeNode? parent)
{ {
Parent = parent; Parent = parent;
Type = visual.GetType().Name; Type = visual.GetType().Name;
Visual = visual; Visual = visual;
_classes = string.Empty;
if (visual is IControl control) if (visual is IControl control)
{ {
@ -51,10 +52,9 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
public TreeNodeCollection Children public abstract TreeNodeCollection Children
{ {
get; get;
protected set;
} }
public string Classes public string Classes
@ -63,7 +63,7 @@ namespace Avalonia.Diagnostics.ViewModels
private set { RaiseAndSetIfChanged(ref _classes, value); } private set { RaiseAndSetIfChanged(ref _classes, value); }
} }
public string ElementName public string? ElementName
{ {
get; get;
} }
@ -79,7 +79,7 @@ namespace Avalonia.Diagnostics.ViewModels
set { RaiseAndSetIfChanged(ref _isExpanded, value); } set { RaiseAndSetIfChanged(ref _isExpanded, value); }
} }
public TreeNode Parent public TreeNode? Parent
{ {
get; get;
} }
@ -92,7 +92,7 @@ namespace Avalonia.Diagnostics.ViewModels
public void Dispose() public void Dispose()
{ {
_classesSubscription.Dispose(); _classesSubscription?.Dispose();
Children.Dispose(); Children.Dispose();
} }

39
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs

@ -3,46 +3,33 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Collections; using Avalonia.Collections;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList<TreeNode>, IDisposable internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList<TreeNode>, IDisposable
{ {
private AvaloniaList<TreeNode> _inner; private AvaloniaList<TreeNode>? _inner;
public TreeNodeCollection(TreeNode owner) => Owner = owner; public TreeNodeCollection(TreeNode owner) => Owner = owner;
public TreeNode this[int index] public TreeNode this[int index] => EnsureInitialized()[index];
{
get
{
EnsureInitialized();
return _inner[index];
}
}
public int Count public int Count => EnsureInitialized().Count;
{
get
{
EnsureInitialized();
return _inner.Count;
}
}
protected TreeNode Owner { get; } protected TreeNode Owner { get; }
public event NotifyCollectionChangedEventHandler CollectionChanged public event NotifyCollectionChangedEventHandler? CollectionChanged
{ {
add => _inner.CollectionChanged += value; add => EnsureInitialized().CollectionChanged += value;
remove => _inner.CollectionChanged -= value; remove => EnsureInitialized().CollectionChanged -= value;
} }
public event PropertyChangedEventHandler PropertyChanged public event PropertyChangedEventHandler? PropertyChanged
{ {
add => _inner.PropertyChanged += value; add => EnsureInitialized().PropertyChanged += value;
remove => _inner.PropertyChanged -= value; remove => EnsureInitialized().PropertyChanged -= value;
} }
public virtual void Dispose() public virtual void Dispose()
@ -58,21 +45,21 @@ namespace Avalonia.Diagnostics.ViewModels
public IEnumerator<TreeNode> GetEnumerator() public IEnumerator<TreeNode> GetEnumerator()
{ {
EnsureInitialized(); return EnsureInitialized().GetEnumerator();
return _inner.GetEnumerator();
} }
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
protected abstract void Initialize(AvaloniaList<TreeNode> nodes); protected abstract void Initialize(AvaloniaList<TreeNode> nodes);
private void EnsureInitialized() private AvaloniaList<TreeNode> EnsureInitialized()
{ {
if (_inner is null) if (_inner is null)
{ {
_inner = new AvaloniaList<TreeNode>(); _inner = new AvaloniaList<TreeNode>();
Initialize(_inner); Initialize(_inner);
} }
return _inner;
} }
} }
} }

110
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs

@ -1,53 +1,50 @@
using System; using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text.RegularExpressions;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
internal class TreePageViewModel : ViewModelBase, IDisposable, INotifyDataErrorInfo internal class TreePageViewModel : ViewModelBase, IDisposable
{ {
private readonly Dictionary<string, string> _errors = new Dictionary<string, string>(); private TreeNode? _selectedNode;
private TreeNode _selectedNode; private ControlDetailsViewModel? _details;
private ControlDetailsViewModel _details;
private string _propertyFilter = string.Empty;
private bool _useRegexFilter;
public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes) public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes)
{ {
MainView = mainView; MainView = mainView;
Nodes = nodes; Nodes = nodes;
PropertiesFilter = new FilterViewModel();
PropertiesFilter.RefreshFilter += (s, e) => Details?.PropertiesView.Refresh();
SettersFilter = new FilterViewModel();
SettersFilter.RefreshFilter += (s, e) => Details?.UpdateStyleFilters();
} }
public MainViewModel MainView { get; } public MainViewModel MainView { get; }
public FilterViewModel PropertiesFilter { get; }
public FilterViewModel SettersFilter { get; }
public TreeNode[] Nodes { get; protected set; } public TreeNode[] Nodes { get; protected set; }
public TreeNode SelectedNode public TreeNode? SelectedNode
{ {
get => _selectedNode; get => _selectedNode;
private set private set
{ {
var oldDetails = Details;
if (RaiseAndSetIfChanged(ref _selectedNode, value)) if (RaiseAndSetIfChanged(ref _selectedNode, value))
{ {
Details = value != null ? Details = value != null ?
new ControlDetailsViewModel(this, value.Visual) : new ControlDetailsViewModel(this, value.Visual) :
null; null;
Details?.UpdateStyleFilters();
if (Details != null && oldDetails != null)
{
Details.StyleFilter = oldDetails.StyleFilter;
}
} }
} }
} }
public ControlDetailsViewModel Details public ControlDetailsViewModel? Details
{ {
get => _details; get => _details;
private set private set
@ -61,63 +58,6 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
public Regex FilterRegex { get; set; }
private void UpdateFilterRegex()
{
void ClearError()
{
if (_errors.Remove(nameof(PropertyFilter)))
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(PropertyFilter)));
}
}
if (UseRegexFilter)
{
try
{
FilterRegex = new Regex(PropertyFilter, RegexOptions.Compiled);
ClearError();
}
catch (Exception exception)
{
_errors[nameof(PropertyFilter)] = exception.Message;
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(PropertyFilter)));
}
}
else
{
ClearError();
}
}
public string PropertyFilter
{
get => _propertyFilter;
set
{
if (RaiseAndSetIfChanged(ref _propertyFilter, value))
{
UpdateFilterRegex();
Details.PropertiesView.Refresh();
}
}
}
public bool UseRegexFilter
{
get => _useRegexFilter;
set
{
if (RaiseAndSetIfChanged(ref _useRegexFilter, value))
{
UpdateFilterRegex();
Details.PropertiesView.Refresh();
}
}
}
public void Dispose() public void Dispose()
{ {
foreach (var node in Nodes) foreach (var node in Nodes)
@ -128,7 +68,7 @@ namespace Avalonia.Diagnostics.ViewModels
_details?.Dispose(); _details?.Dispose();
} }
public TreeNode FindNode(IControl control) public TreeNode? FindNode(IControl control)
{ {
foreach (var node in Nodes) foreach (var node in Nodes)
{ {
@ -164,7 +104,7 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
private void ExpandNode(TreeNode node) private void ExpandNode(TreeNode? node)
{ {
if (node != null) if (node != null)
{ {
@ -173,7 +113,7 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
private TreeNode FindNode(TreeNode node, IControl control) private TreeNode? FindNode(TreeNode node, IControl control)
{ {
if (node.Visual == control) if (node.Visual == control)
{ {
@ -194,17 +134,5 @@ namespace Avalonia.Diagnostics.ViewModels
return null; return null;
} }
public IEnumerable GetErrors(string propertyName)
{
if (_errors.TryGetValue(propertyName, out var error))
{
yield return error;
}
}
public bool HasErrors => _errors.Count > 0;
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
} }
} }

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

@ -1,16 +1,16 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
internal class ViewModelBase : INotifyPropertyChanged internal class ViewModelBase : INotifyPropertyChanged
{ {
private PropertyChangedEventHandler _propertyChanged; private PropertyChangedEventHandler? _propertyChanged;
private List<string> events = new List<string>(); private List<string> events = new List<string>();
public event PropertyChangedEventHandler PropertyChanged public event PropertyChangedEventHandler? PropertyChanged
{ {
add { _propertyChanged += value; events.Add("added"); } add { _propertyChanged += value; events.Add("added"); }
remove { _propertyChanged -= value; events.Add("removed"); } remove { _propertyChanged -= value; events.Add("removed"); }
@ -20,7 +20,7 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
} }
protected bool RaiseAndSetIfChanged<T>(ref T field, T value, [CallerMemberName] string propertyName = null) protected bool RaiseAndSetIfChanged<T>([NotNullIfNotNull("value")] ref T field, T value, [CallerMemberName] string propertyName = null!)
{ {
if (!EqualityComparer<T>.Default.Equals(field, value)) if (!EqualityComparer<T>.Default.Equals(field, value))
{ {
@ -32,7 +32,7 @@ namespace Avalonia.Diagnostics.ViewModels
return false; return false;
} }
protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) protected void RaisePropertyChanged([CallerMemberName] string propertyName = null!)
{ {
var e = new PropertyChangedEventArgs(propertyName); var e = new PropertyChangedEventArgs(propertyName);
OnPropertyChanged(e); OnPropertyChanged(e);

8
src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs

@ -7,7 +7,7 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
internal class VisualTreeNode : TreeNode internal class VisualTreeNode : TreeNode
{ {
public VisualTreeNode(IVisual visual, TreeNode parent) public VisualTreeNode(IVisual visual, TreeNode? parent)
: base(visual, parent) : base(visual, parent)
{ {
Children = new VisualTreeNodeCollection(this, visual); Children = new VisualTreeNodeCollection(this, visual);
@ -20,16 +20,18 @@ namespace Avalonia.Diagnostics.ViewModels
public bool IsInTemplate { get; private set; } public bool IsInTemplate { get; private set; }
public override TreeNodeCollection Children { get; }
public static VisualTreeNode[] Create(object control) public static VisualTreeNode[] Create(object control)
{ {
var visual = control as IVisual; var visual = control as IVisual;
return visual != null ? new[] { new VisualTreeNode(visual, null) } : null; return visual != null ? new[] { new VisualTreeNode(visual, null) } : Array.Empty<VisualTreeNode>();
} }
internal class VisualTreeNodeCollection : TreeNodeCollection internal class VisualTreeNodeCollection : TreeNodeCollection
{ {
private readonly IVisual _control; private readonly IVisual _control;
private IDisposable _subscription; private IDisposable? _subscription;
public VisualTreeNodeCollection(TreeNode owner, IVisual control) public VisualTreeNodeCollection(TreeNode owner, IVisual control)
: base(owner) : base(owner)

14
src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs

@ -30,22 +30,26 @@ namespace Avalonia.Diagnostics.Views
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
private void HistoryChanged(object sender, NotifyCollectionChangedEventArgs e) private void HistoryChanged(object? sender, NotifyCollectionChangedEventArgs e)
{ {
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems[0] is IControl control) if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems?[0] is IControl control)
{ {
DispatcherTimer.RunOnce(control.BringIntoView, TimeSpan.Zero); DispatcherTimer.RunOnce(control.BringIntoView, TimeSpan.Zero);
} }
} }
private void InputKeyDown(object sender, KeyEventArgs e) private void InputKeyDown(object? sender, KeyEventArgs e)
{ {
var vm = (ConsoleViewModel)DataContext; var vm = (ConsoleViewModel?)DataContext;
if (vm is null)
{
return;
}
switch (e.Key) switch (e.Key)
{ {
case Key.Enter: case Key.Enter:
vm.Execute(); _ = vm.Execute();
e.Handled = true; e.Handled = true;
break; break;
case Key.Up: case Key.Up:

36
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters" xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
xmlns:local="clr-namespace:Avalonia.Diagnostics.Views" xmlns:local="clr-namespace:Avalonia.Diagnostics.Views"
xmlns:controls="clr-namespace:Avalonia.Diagnostics.Controls"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
x:Class="Avalonia.Diagnostics.Views.ControlDetailsView" x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"
x:Name="Main"> x:Name="Main">
@ -12,24 +13,19 @@
<Grid ColumnDefinitions="*,Auto,320"> <Grid ColumnDefinitions="*,Auto,320">
<Grid Grid.Column="0" ColumnDefinitions="*,Auto,Auto,Auto" RowDefinitions="Auto,*"> <Grid Grid.Column="0" RowDefinitions="Auto,*">
<TextBox Grid.Row="0" <controls:FilterTextBox Grid.Row="0"
Grid.Column="0" BorderThickness="0"
BorderThickness="0" DataContext="{Binding TreePage.PropertiesFilter}"
Text="{Binding TreePage.PropertyFilter}" Text="{Binding FilterString}"
Watermark="Filter properties" /> Watermark="Filter properties"
UseCaseSensitiveFilter="{Binding UseCaseSensitiveFilter}"
<CheckBox Grid.Row="0" UseWholeWordFilter="{Binding UseWholeWordFilter}"
Grid.Column="1" UseRegexFilter="{Binding UseRegexFilter}" />
Margin="0,0,4,0"
Content="Regex"
IsChecked="{Binding TreePage.UseRegexFilter}" />
<DataGrid Items="{Binding PropertiesView}" <DataGrid Items="{Binding PropertiesView}"
Grid.Row="1" Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
BorderThickness="0" BorderThickness="0"
RowBackground="Transparent" RowBackground="Transparent"
SelectedItem="{Binding SelectedProperty, Mode=TwoWay}" SelectedItem="{Binding SelectedProperty, Mode=TwoWay}"
@ -71,7 +67,13 @@
<ToggleButton Margin="2,0,0,0" Grid.Column="3" ToolTip.Tip="Snapshot current styles (Alt+S/Alt+D to enable/disable within debugged window)" Content="Snapshot" IsChecked="{Binding SnapshotStyles}" /> <ToggleButton Margin="2,0,0,0" Grid.Column="3" ToolTip.Tip="Snapshot current styles (Alt+S/Alt+D to enable/disable within debugged window)" Content="Snapshot" IsChecked="{Binding SnapshotStyles}" />
</Grid> </Grid>
<TextBox Grid.Row="1" Margin="2" Grid.Column="0" Watermark="Filter" Text="{Binding StyleFilter}" /> <controls:FilterTextBox Grid.Row="1" Margin="2" Grid.Column="0"
DataContext="{Binding TreePage.SettersFilter}"
Text="{Binding FilterString}"
Watermark="Filter setters"
UseCaseSensitiveFilter="{Binding UseCaseSensitiveFilter}"
UseWholeWordFilter="{Binding UseWholeWordFilter}"
UseRegexFilter="{Binding UseRegexFilter}" />
</Grid> </Grid>
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Disabled"> <ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Disabled">

9
src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml

@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters" xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
xmlns:controls="clr-namespace:Avalonia.Diagnostics.Controls"
x:Class="Avalonia.Diagnostics.Views.EventsPageView" x:Class="Avalonia.Diagnostics.Views.EventsPageView"
Margin="2"> Margin="2">
<UserControl.Styles> <UserControl.Styles>
@ -35,7 +36,13 @@
<Grid Grid.Column="0" RowDefinitions="Auto,*,Auto"> <Grid Grid.Column="0" RowDefinitions="Auto,*,Auto">
<TextBox Classes="clearButton" Grid.Row="0" Margin="0,0,0,2" Text="{Binding EventTypeFilter}" Watermark="Search event types" /> <controls:FilterTextBox Grid.Row="0" Margin="0,0,0,2"
DataContext="{Binding EventsFilter}"
Text="{Binding FilterString}"
Watermark="Filter events"
UseCaseSensitiveFilter="{Binding UseCaseSensitiveFilter}"
UseWholeWordFilter="{Binding UseWholeWordFilter}"
UseRegexFilter="{Binding UseRegexFilter}" />
<TreeView Grid.Row="1" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}" > <TreeView Grid.Row="1" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}" >
<TreeView.DataTemplates> <TreeView.DataTemplates>

2
src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs

@ -53,7 +53,7 @@ namespace Avalonia.Diagnostics.Views
} }
} }
private void OnRecordedEventsChanged(object sender, NotifyCollectionChangedEventArgs e) private void OnRecordedEventsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{ {
if (sender is ObservableCollection<FiredEvent> events) if (sender is ObservableCollection<FiredEvent> events)
{ {

8
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs

@ -27,7 +27,11 @@ namespace Avalonia.Diagnostics.Views
public void ToggleConsole() public void ToggleConsole()
{ {
var vm = (MainViewModel)DataContext; var vm = (MainViewModel?)DataContext;
if (vm is null)
{
return;
}
if (_consoleHeight == -1) if (_consoleHeight == -1)
{ {
@ -54,7 +58,7 @@ namespace Avalonia.Diagnostics.Views
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
private void PreviewKeyDown(object sender, KeyEventArgs e) private void PreviewKeyDown(object? sender, KeyEventArgs e)
{ {
if (e.Key == Key.Escape) if (e.Key == Key.Escape)
{ {

1
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml

@ -16,6 +16,7 @@
<Setter Property="Foreground" Value="Black"/> <Setter Property="Foreground" Value="Black"/>
</Style> </Style>
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml" /> <StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml" />
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml" />
</Window.Styles> </Window.Styles>
<views:MainView/> <views:MainView/>

33
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs

@ -15,7 +15,7 @@ namespace Avalonia.Diagnostics.Views
internal class MainWindow : Window, IStyleHost internal class MainWindow : Window, IStyleHost
{ {
private readonly IDisposable _keySubscription; private readonly IDisposable _keySubscription;
private TopLevel _root; private TopLevel? _root;
public MainWindow() public MainWindow()
{ {
@ -26,7 +26,7 @@ namespace Avalonia.Diagnostics.Views
.Subscribe(RawKeyDown); .Subscribe(RawKeyDown);
} }
public TopLevel Root public TopLevel? Root
{ {
get => _root; get => _root;
set set
@ -43,7 +43,7 @@ namespace Avalonia.Diagnostics.Views
if (_root != null) if (_root != null)
{ {
_root.Closed += RootClosed; _root.Closed += RootClosed;
DataContext = new MainViewModel(value); DataContext = new MainViewModel(_root);
} }
else else
{ {
@ -53,15 +53,20 @@ namespace Avalonia.Diagnostics.Views
} }
} }
IStyleHost IStyleHost.StylingParent => null; IStyleHost? IStyleHost.StylingParent => null;
protected override void OnClosed(EventArgs e) protected override void OnClosed(EventArgs e)
{ {
base.OnClosed(e); base.OnClosed(e);
_keySubscription.Dispose(); _keySubscription.Dispose();
_root.Closed -= RootClosed;
_root = null; if (_root != null)
((MainViewModel)DataContext)?.Dispose(); {
_root.Closed -= RootClosed;
_root = null;
}
((MainViewModel?)DataContext)?.Dispose();
} }
private void InitializeComponent() private void InitializeComponent()
@ -71,12 +76,20 @@ namespace Avalonia.Diagnostics.Views
private void RawKeyDown(RawKeyEventArgs e) private void RawKeyDown(RawKeyEventArgs e)
{ {
var vm = (MainViewModel?)DataContext;
if (vm is null)
{
return;
}
const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift; const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift;
if (e.Modifiers == modifiers) if (e.Modifiers == modifiers)
{ {
#pragma warning disable CS0618 // Type or member is obsolete
var point = (Root as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default; var point = (Root as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default;
#pragma warning restore CS0618 // Type or member is obsolete
var control = Root.GetVisualsAt(point, x => var control = Root.GetVisualsAt(point, x =>
{ {
if (x is AdornerLayer || !x.IsVisible) return false; if (x is AdornerLayer || !x.IsVisible) return false;
@ -87,7 +100,6 @@ namespace Avalonia.Diagnostics.Views
if (control != null) if (control != null)
{ {
var vm = (MainViewModel)DataContext;
vm.SelectControl((IControl)control); vm.SelectControl((IControl)control);
} }
} }
@ -97,12 +109,11 @@ namespace Avalonia.Diagnostics.Views
{ {
var enable = e.Key == Key.S; var enable = e.Key == Key.S;
var vm = (MainViewModel)DataContext;
vm.EnableSnapshotStyles(enable); vm.EnableSnapshotStyles(enable);
} }
} }
} }
private void RootClosed(object sender, EventArgs e) => Close(); private void RootClosed(object? sender, EventArgs e) => Close();
} }
} }

24
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs

@ -14,12 +14,13 @@ namespace Avalonia.Diagnostics.Views
internal class TreePageView : UserControl internal class TreePageView : UserControl
{ {
private readonly Panel _adorner; private readonly Panel _adorner;
private AdornerLayer _currentLayer; private AdornerLayer? _currentLayer;
private TreeView _tree; private TreeView _tree;
public TreePageView() public TreePageView()
{ {
InitializeComponent(); InitializeComponent();
_tree = this.FindControl<TreeView>("tree");
_tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized; _tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized;
_adorner = new Panel _adorner = new Panel
@ -37,9 +38,15 @@ namespace Avalonia.Diagnostics.Views
}; };
} }
protected void AddAdorner(object sender, PointerEventArgs e) protected void AddAdorner(object? sender, PointerEventArgs e)
{ {
var node = (TreeNode)((Control)sender).DataContext; var node = (TreeNode?)((Control)sender!).DataContext;
var vm = (TreePageViewModel?)DataContext;
if (node is null || vm is null)
{
return;
}
var visual = (Visual)node.Visual; var visual = (Visual)node.Visual;
_currentLayer = AdornerLayer.GetAdornerLayer(visual); _currentLayer = AdornerLayer.GetAdornerLayer(visual);
@ -53,8 +60,6 @@ namespace Avalonia.Diagnostics.Views
_currentLayer.Children.Add(_adorner); _currentLayer.Children.Add(_adorner);
AdornerLayer.SetAdornedElement(_adorner, visual); AdornerLayer.SetAdornedElement(_adorner, visual);
var vm = (TreePageViewModel) DataContext;
if (vm.MainView.ShouldVisualizeMarginPadding) if (vm.MainView.ShouldVisualizeMarginPadding)
{ {
var paddingBorder = (Border)_adorner.Children[0]; var paddingBorder = (Border)_adorner.Children[0];
@ -74,7 +79,7 @@ namespace Avalonia.Diagnostics.Views
return new Thickness(-input.Left, -input.Top, -input.Right, -input.Bottom); return new Thickness(-input.Left, -input.Top, -input.Right, -input.Bottom);
} }
protected void RemoveAdorner(object sender, PointerEventArgs e) protected void RemoveAdorner(object? sender, PointerEventArgs e)
{ {
foreach (var border in _adorner.Children.OfType<Border>()) foreach (var border in _adorner.Children.OfType<Border>())
{ {
@ -90,18 +95,17 @@ namespace Avalonia.Diagnostics.Views
private void InitializeComponent() private void InitializeComponent()
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
_tree = this.FindControl<TreeView>("tree");
} }
private void TreeViewItemMaterialized(object sender, ItemContainerEventArgs e) private void TreeViewItemMaterialized(object? sender, ItemContainerEventArgs e)
{ {
var item = (TreeViewItem)e.Containers[0].ContainerControl; var item = (TreeViewItem)e.Containers[0].ContainerControl;
item.TemplateApplied += TreeViewItemTemplateApplied; item.TemplateApplied += TreeViewItemTemplateApplied;
} }
private void TreeViewItemTemplateApplied(object sender, TemplateAppliedEventArgs e) private void TreeViewItemTemplateApplied(object? sender, TemplateAppliedEventArgs e)
{ {
var item = (TreeViewItem)sender; var item = (TreeViewItem)sender!;
// This depends on the default tree item template. // This depends on the default tree item template.
// We want to handle events in the item header but exclude events coming from children. // We want to handle events in the item header but exclude events coming from children.

2
src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs

@ -17,7 +17,7 @@ namespace Avalonia.Diagnostics
private static void PrintVisualTree(IVisual visual, StringBuilder builder, int indent) private static void PrintVisualTree(IVisual visual, StringBuilder builder, int indent)
{ {
Control control = visual as Control; Control? control = visual as Control;
builder.Append(Indent(indent - 1)); builder.Append(Indent(indent - 1));

2
src/Avalonia.Input/IInputElement.cs

@ -3,6 +3,8 @@ using System.Collections.Generic;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.VisualTree; using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Input namespace Avalonia.Input
{ {
/// <summary> /// <summary>

2
src/Avalonia.Input/InputElement.cs

@ -9,6 +9,8 @@ using Avalonia.Input.TextInput;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.VisualTree; using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Input namespace Avalonia.Input
{ {
/// <summary> /// <summary>

4
src/Avalonia.Input/KeyboardDevice.cs

@ -60,7 +60,7 @@ namespace Avalonia.Input
ie.IsKeyboardFocusWithin = false; ie.IsKeyboardFocusWithin = false;
} }
el = (IInputElement)el.VisualParent; el = (IInputElement?)el.VisualParent;
} }
} }
@ -212,7 +212,7 @@ namespace Avalonia.Input
Source = element, Source = element,
}; };
IVisual currentHandler = element; IVisual? currentHandler = element;
while (currentHandler != null && !ev.Handled && keyInput.Type == RawKeyEventType.KeyDown) while (currentHandler != null && !ev.Handled && keyInput.Type == RawKeyEventType.KeyDown)
{ {
var bindings = (currentHandler as IInputElement)?.KeyBindings; var bindings = (currentHandler as IInputElement)?.KeyBindings;

8
src/Avalonia.Input/MouseDevice.cs

@ -377,7 +377,7 @@ namespace Avalonia.Input
e.Source = element; e.Source = element;
e.Handled = false; e.Handled = false;
element.RaiseEvent(e); element.RaiseEvent(e);
element = (IInputElement)element.VisualParent; element = (IInputElement?)element.VisualParent;
} }
root.PointerOverElement = null; root.PointerOverElement = null;
@ -444,7 +444,7 @@ namespace Avalonia.Input
branch = el; branch = el;
break; break;
} }
el = (IInputElement)el.VisualParent; el = (IInputElement?)el.VisualParent;
} }
el = root.PointerOverElement; el = root.PointerOverElement;
@ -460,7 +460,7 @@ namespace Avalonia.Input
e.Source = el; e.Source = el;
e.Handled = false; e.Handled = false;
el.RaiseEvent(e); el.RaiseEvent(e);
el = (IInputElement)el.VisualParent; el = (IInputElement?)el.VisualParent;
} }
el = root.PointerOverElement = element; el = root.PointerOverElement = element;
@ -471,7 +471,7 @@ namespace Avalonia.Input
e.Source = el; e.Source = el;
e.Handled = false; e.Handled = false;
el.RaiseEvent(e); el.RaiseEvent(e);
el = (IInputElement)el.VisualParent; el = (IInputElement?)el.VisualParent;
} }
} }

2
src/Avalonia.Interactivity/IInteractive.cs

@ -1,5 +1,7 @@
using System; using System;
#nullable enable
namespace Avalonia.Interactivity namespace Avalonia.Interactivity
{ {
/// <summary> /// <summary>

2
src/Avalonia.Interactivity/Interactive.cs

@ -3,6 +3,8 @@ using System.Collections.Generic;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.VisualTree; using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Interactivity namespace Avalonia.Interactivity
{ {
/// <summary> /// <summary>

2
src/Avalonia.Layout/Layoutable.cs

@ -782,7 +782,7 @@ namespace Avalonia.Layout
} }
/// <inheritdoc/> /// <inheritdoc/>
protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent) protected sealed override void OnVisualParentChanged(IVisual? oldParent, IVisual? newParent)
{ {
LayoutHelper.InvalidateSelfAndChildrenMeasure(this); LayoutHelper.InvalidateSelfAndChildrenMeasure(this);

6
src/Avalonia.Styling/Controls/ISetInheritanceParent.cs

@ -1,3 +1,5 @@
#nullable enable
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
@ -14,6 +16,6 @@ namespace Avalonia.Controls
/// Sets the control's inheritance parent. /// Sets the control's inheritance parent.
/// </summary> /// </summary>
/// <param name="parent">The parent.</param> /// <param name="parent">The parent.</param>
void SetParent(IAvaloniaObject parent); void SetParent(IAvaloniaObject? parent);
} }
} }

6
src/Avalonia.Styling/Controls/ISetLogicalParent.cs

@ -1,5 +1,7 @@
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
#nullable enable
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
@ -14,6 +16,6 @@ namespace Avalonia.Controls
/// Sets the control's parent. /// Sets the control's parent.
/// </summary> /// </summary>
/// <param name="parent">The parent.</param> /// <param name="parent">The parent.</param>
void SetParent(ILogical parent); void SetParent(ILogical? parent);
} }
} }

6
src/Avalonia.Styling/IDataContextProvider.cs

@ -1,4 +1,6 @@
namespace Avalonia #nullable enable
namespace Avalonia
{ {
/// <summary> /// <summary>
/// Defines an element with a data context that can be used for binding. /// Defines an element with a data context that can be used for binding.
@ -8,6 +10,6 @@
/// <summary> /// <summary>
/// Gets or sets the element's data context. /// Gets or sets the element's data context.
/// </summary> /// </summary>
object DataContext { get; set; } object? DataContext { get; set; }
} }
} }

6
src/Avalonia.Styling/IStyledElement.cs

@ -4,6 +4,8 @@ using Avalonia.Controls;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Styling; using Avalonia.Styling;
#nullable enable
namespace Avalonia namespace Avalonia
{ {
public interface IStyledElement : public interface IStyledElement :
@ -17,7 +19,7 @@ namespace Avalonia
/// <summary> /// <summary>
/// Occurs when the control has finished initialization. /// Occurs when the control has finished initialization.
/// </summary> /// </summary>
event EventHandler Initialized; event EventHandler? Initialized;
/// <summary> /// <summary>
/// Gets a value that indicates whether the element has finished initialization. /// Gets a value that indicates whether the element has finished initialization.
@ -32,6 +34,6 @@ namespace Avalonia
/// <summary> /// <summary>
/// Gets the control's logical parent. /// Gets the control's logical parent.
/// </summary> /// </summary>
IStyledElement Parent { get; } IStyledElement? Parent { get; }
} }
} }

4
src/Avalonia.Styling/StyledElement.cs

@ -300,7 +300,7 @@ namespace Avalonia
bool IStyleHost.IsStylesInitialized => _styles != null; bool IStyleHost.IsStylesInitialized => _styles != null;
/// <inheritdoc/> /// <inheritdoc/>
IStyleHost? IStyleHost.StylingParent => (IStyleHost)InheritanceParent; IStyleHost? IStyleHost.StylingParent => (IStyleHost?)InheritanceParent;
/// <inheritdoc/> /// <inheritdoc/>
public virtual void BeginInit() public virtual void BeginInit()
@ -465,7 +465,7 @@ namespace Avalonia
/// Sets the styled element's inheritance parent. /// Sets the styled element's inheritance parent.
/// </summary> /// </summary>
/// <param name="parent">The parent.</param> /// <param name="parent">The parent.</param>
void ISetInheritanceParent.SetParent(IAvaloniaObject parent) void ISetInheritanceParent.SetParent(IAvaloniaObject? parent)
{ {
InheritanceParent = parent; InheritanceParent = parent;
} }

4
src/Avalonia.Styling/Styling/ISetterValue.cs

@ -1,4 +1,6 @@
namespace Avalonia.Styling #nullable enable
namespace Avalonia.Styling
{ {
/// <summary> /// <summary>
/// Customizes the behavior of a class when added as a value to an <see cref="ISetter"/>. /// Customizes the behavior of a class when added as a value to an <see cref="ISetter"/>.

4
src/Avalonia.Styling/Styling/PropertySetterInstance.cs

@ -50,7 +50,7 @@ namespace Avalonia.Styling
} }
else else
{ {
_subscription = _target.Bind(_directProperty, this); _subscription = _target.Bind(_directProperty!, this);
} }
} }
else else
@ -100,7 +100,7 @@ namespace Avalonia.Styling
} }
else else
{ {
_target.ClearValue(_directProperty); _target.ClearValue(_directProperty!);
} }
} }

4
src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs

@ -53,7 +53,7 @@ namespace Avalonia.Styling
} }
else else
{ {
_subscription = _target.Bind(_directProperty, this); _subscription = _target.Bind(_directProperty!, this);
} }
} }
@ -91,7 +91,7 @@ namespace Avalonia.Styling
} }
else else
{ {
_target.ClearValue(_directProperty); _target.ClearValue(_directProperty!);
} }
} }

7
src/Avalonia.Themes.Default/TextBox.xaml

@ -113,13 +113,13 @@
</Style> </Style>
<Style Selector="Button.textBoxClearButton"> <Style Selector="Button.textBoxClearButton">
<Setter Property="Cursor" Value="Arrow" />
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="Button"> <ControlTemplate TargetType="Button">
<Border x:Name="PART_ButtonLayoutBorder" <Border x:Name="PART_ButtonLayoutBorder"
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
Background="Transparent" Background="Transparent">
Cursor="Arrow">
<Path x:Name="PART_GlyphElement" <Path x:Name="PART_GlyphElement"
Fill="{DynamicResource ThemeForegroundColor}" Fill="{DynamicResource ThemeForegroundColor}"
Data="{StaticResource TextBoxClearButtonData}" Data="{StaticResource TextBoxClearButtonData}"
@ -146,13 +146,14 @@
<Setter Property="(ToolTip.Tip)" Value="Show Password" /> <Setter Property="(ToolTip.Tip)" Value="Show Password" />
</Style> </Style>
</Panel.Styles> </Panel.Styles>
<ToggleButton Classes="passwordBoxRevealButton" Background="Transparent" Cursor="Arrow" IsChecked="{Binding $parent[TextBox].RevealPassword, Mode=TwoWay}" /> <ToggleButton Classes="passwordBoxRevealButton" Background="Transparent" IsChecked="{Binding $parent[TextBox].RevealPassword, Mode=TwoWay}" />
</Panel> </Panel>
</Template> </Template>
</Setter> </Setter>
</Style> </Style>
<Style Selector="ToggleButton.passwordBoxRevealButton"> <Style Selector="ToggleButton.passwordBoxRevealButton">
<Setter Property="Cursor" Value="Arrow" />
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="ToggleButton"> <ControlTemplate TargetType="ToggleButton">

13
src/Avalonia.Visuals/Animation/Transitions/BoxShadowsTransition.cs

@ -1,6 +1,3 @@
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
using Avalonia.Media; using Avalonia.Media;
@ -9,15 +6,7 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="BoxShadows"/> type. /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="BoxShadows"/> type.
/// </summary> /// </summary>
public class BoxShadowsTransition : Transition<BoxShadows> public class BoxShadowsTransition : AnimatorDrivenTransition<BoxShadows, BoxShadowsAnimator>
{ {
private static readonly BoxShadowsAnimator s_animator = new BoxShadowsAnimator();
/// <inheritdocs/>
public override IObservable<BoxShadows> DoTransition(IObservable<double> progress, BoxShadows oldValue, BoxShadows newValue)
{
return progress
.Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
} }
} }

59
src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs

@ -0,0 +1,59 @@
using System;
using Avalonia.Animation.Animators;
using Avalonia.Animation.Easings;
using Avalonia.Media;
#nullable enable
namespace Avalonia.Animation
{
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="IBrush"/> type.
/// Only values of <see cref="ISolidColorBrush"/> will transition correctly at the moment.
/// </summary>
public class BrushTransition : Transition<IBrush?>
{
private static readonly ISolidColorBrushAnimator s_animator = new ISolidColorBrushAnimator();
public override IObservable<IBrush?> DoTransition(IObservable<double> progress, IBrush? oldValue, IBrush? newValue)
{
var oldSolidColorBrush = TryGetSolidColorBrush(oldValue);
var newSolidColorBrush = TryGetSolidColorBrush(newValue);
if (oldSolidColorBrush != null && newSolidColorBrush != null)
{
return new AnimatorTransitionObservable<ISolidColorBrush, ISolidColorBrushAnimator>(
s_animator, progress, Easing, oldSolidColorBrush, newSolidColorBrush);
}
return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue);
}
private static ISolidColorBrush? TryGetSolidColorBrush(IBrush? brush)
{
if (brush is null)
{
return Brushes.Transparent;
}
return brush as ISolidColorBrush;
}
private class IncompatibleTransitionObservable : TransitionObservableBase<IBrush?>
{
private readonly IBrush? _from;
private readonly IBrush? _to;
public IncompatibleTransitionObservable(IObservable<double> progress, Easing easing, IBrush? from, IBrush? to) : base(progress, easing)
{
_from = from;
_to = to;
}
protected override IBrush? ProduceValue(double progress)
{
return progress < 0.5 ? _from : _to;
}
}
}
}

12
src/Avalonia.Visuals/Animation/Transitions/ColorTransition.cs

@ -0,0 +1,12 @@
using Avalonia.Animation.Animators;
using Avalonia.Media;
namespace Avalonia.Animation
{
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Color"/> type.
/// </summary>
public class ColorTransition : AnimatorDrivenTransition<Color, ColorAnimator>
{
}
}

13
src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs

@ -1,6 +1,3 @@
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
namespace Avalonia.Animation namespace Avalonia.Animation
@ -8,15 +5,7 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="CornerRadius"/> type. /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="CornerRadius"/> type.
/// </summary> /// </summary>
public class CornerRadiusTransition : Transition<CornerRadius> public class CornerRadiusTransition : AnimatorDrivenTransition<CornerRadius, CornerRadiusAnimator>
{ {
private static readonly CornerRadiusAnimator s_animator = new CornerRadiusAnimator();
/// <inheritdocs/>
public override IObservable<CornerRadius> DoTransition(IObservable<double> progress, CornerRadius oldValue, CornerRadius newValue)
{
return progress
.Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
} }
} }

13
src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs

@ -1,6 +1,3 @@
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
namespace Avalonia.Animation namespace Avalonia.Animation
@ -8,15 +5,7 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Point"/> type. /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Point"/> type.
/// </summary> /// </summary>
public class PointTransition : Transition<Point> public class PointTransition : AnimatorDrivenTransition<Point, PointAnimator>
{ {
private static readonly PointAnimator s_animator = new PointAnimator();
/// <inheritdocs/>
public override IObservable<Point> DoTransition(IObservable<double> progress, Point oldValue, Point newValue)
{
return progress
.Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
} }
} }

13
src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs

@ -1,6 +1,3 @@
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
namespace Avalonia.Animation namespace Avalonia.Animation
@ -8,15 +5,7 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Size"/> type. /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Size"/> type.
/// </summary> /// </summary>
public class SizeTransition : Transition<Size> public class SizeTransition : AnimatorDrivenTransition<Size, SizeAnimator>
{ {
private static readonly SizeAnimator s_animator = new SizeAnimator();
/// <inheritdocs/>
public override IObservable<Size> DoTransition(IObservable<double> progress, Size oldValue, Size newValue)
{
return progress
.Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
} }
} }

13
src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs

@ -1,6 +1,3 @@
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
namespace Avalonia.Animation namespace Avalonia.Animation
@ -8,15 +5,7 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Thickness"/> type. /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Thickness"/> type.
/// </summary> /// </summary>
public class ThicknessTransition : Transition<Thickness> public class ThicknessTransition : AnimatorDrivenTransition<Thickness, ThicknessAnimator>
{ {
private static readonly ThicknessAnimator s_animator = new ThicknessAnimator();
/// <inheritdocs/>
public override IObservable<Thickness> DoTransition(IObservable<double> progress, Thickness oldValue, Thickness newValue)
{
return progress
.Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
} }
} }

18
src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs

@ -1,28 +1,26 @@
using System; using System;
using System.Reactive.Linq;
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Transformation;
#nullable enable
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
public class TransformOperationsTransition : Transition<ITransform> public class TransformOperationsTransition : Transition<ITransform>
{ {
private static readonly TransformOperationsAnimator _operationsAnimator = new TransformOperationsAnimator(); private static readonly TransformOperationsAnimator s_operationsAnimator = new TransformOperationsAnimator();
public override IObservable<ITransform> DoTransition(IObservable<double> progress, public override IObservable<ITransform> DoTransition(
IObservable<double> progress,
ITransform oldValue, ITransform oldValue,
ITransform newValue) ITransform newValue)
{ {
var oldTransform = TransformOperationsAnimator.EnsureOperations(oldValue); var oldTransform = TransformOperationsAnimator.EnsureOperations(oldValue);
var newTransform = TransformOperationsAnimator.EnsureOperations(newValue); var newTransform = TransformOperationsAnimator.EnsureOperations(newValue);
return progress return new AnimatorTransitionObservable<TransformOperations, TransformOperationsAnimator>(
.Select(p => s_operationsAnimator, progress, Easing, oldTransform, newTransform);
{
var f = Easing.Ease(p);
return _operationsAnimator.Interpolate(f, oldTransform, newTransform);
});
} }
} }
} }

13
src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs

@ -1,6 +1,3 @@
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
namespace Avalonia.Animation namespace Avalonia.Animation
@ -8,15 +5,7 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Vector"/> type. /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Vector"/> type.
/// </summary> /// </summary>
public class VectorTransition : Transition<Vector> public class VectorTransition : AnimatorDrivenTransition<Vector, VectorAnimator>
{ {
private static readonly VectorAnimator s_animator = new VectorAnimator();
/// <inheritdocs/>
public override IObservable<Vector> DoTransition(IObservable<double> progress, Vector oldValue, Vector newValue)
{
return progress
.Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
} }
} }

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

Loading…
Cancel
Save