Browse Source

Merge branch 'master' into cancellable-animations-expander-fix

pull/6018/head
Max Katz 5 years ago
committed by GitHub
parent
commit
cabfa13020
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      native/Avalonia.Native/src/OSX/window.mm
  2. 5
      samples/ControlCatalog/MainWindow.xaml.cs
  3. 6
      samples/ControlCatalog/Pages/ContextFlyoutPage.axaml
  4. 3
      samples/RenderDemo/MainWindow.xaml
  5. 25
      samples/RenderDemo/Pages/CustomAnimatorPage.xaml
  6. 27
      samples/RenderDemo/Pages/CustomAnimatorPage.xaml.cs
  7. 16
      samples/RenderDemo/Pages/CustomStringAnimator.cs
  8. 36
      samples/RenderDemo/Pages/TransitionsPage.xaml
  9. 29
      src/Avalonia.Animation/Animation.cs
  10. 25
      src/Avalonia.Animation/AnimationInstance`1.cs
  11. 24
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  12. 81
      src/Avalonia.Base/Metadata/NullableAttributes.cs
  13. 3
      src/Avalonia.Controls/ApiCompatBaseline.txt
  14. 4
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  15. 4
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  16. 3
      src/Avalonia.Controls/ExperimentalAcrylicBorder.cs
  17. 32
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  18. 22
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  19. 22
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  20. 2
      src/Avalonia.Controls/Remote/RemoteWidget.cs
  21. 36
      src/Avalonia.Controls/ScrollViewer.cs
  22. 112
      src/Avalonia.Controls/TextBox.cs
  23. 3
      src/Avalonia.Controls/TickBar.cs
  24. 5
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  25. 11
      src/Avalonia.Controls/Utils/UndoRedoHelper.cs
  26. 4
      src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
  27. 12
      src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml
  28. 8
      src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs
  29. 11
      src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs
  30. 4
      src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs
  31. 14
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  32. 5
      src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
  33. 4
      src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs
  34. 4
      src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
  35. 4
      src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs
  36. 49
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
  37. 19
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
  38. 3
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs
  39. 35
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  40. 12
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs
  41. 2
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs
  42. 23
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
  43. 6
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs
  44. 11
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs
  45. 22
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs
  46. 15
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
  47. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs
  48. 32
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  49. 12
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
  50. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs
  51. 10
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
  52. 16
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  53. 39
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs
  54. 14
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  55. 12
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs
  56. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
  57. 14
      src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs
  58. 2
      src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs
  59. 8
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs
  60. 53
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  61. 24
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
  62. 2
      src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs
  63. 23
      src/Avalonia.Input/KeyboardDevice.cs
  64. 2
      src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs
  65. 32
      src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs
  66. 3
      src/Avalonia.Themes.Default/ListBox.xaml
  67. 3
      src/Avalonia.Themes.Default/TextBox.xaml
  68. 3
      src/Avalonia.Themes.Default/TreeView.xaml
  69. 6
      src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
  70. 3
      src/Avalonia.Themes.Fluent/Controls/ListBox.xaml
  71. 3
      src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
  72. 3
      src/Avalonia.Themes.Fluent/Controls/TreeView.xaml
  73. 59
      src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs
  74. 3
      src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs
  75. 61
      src/Avalonia.Visuals/Media/PolyLineSegment.cs
  76. 5
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  77. 2
      src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs
  78. 88
      src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs
  79. 47
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  80. 13
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  81. 4
      src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs
  82. 32
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  83. 7
      src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs
  84. 11
      src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs
  85. 2
      src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs
  86. 46
      tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs
  87. 40
      tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs
  88. 36
      tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs
  89. 34
      tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs
  90. 23
      tests/Avalonia.Styling.UnitTests/SelectorTests_PropertyEquals.cs
  91. 6
      tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs
  92. BIN
      tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_DrawingContext.expected.png
  93. BIN
      tests/TestFiles/Direct2D1/Media/LinearGradientBrush/LinearGradientBrush_DrawingContext.expected.png
  94. BIN
      tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_DrawingContext.expected.png
  95. BIN
      tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_DrawingContext.expected.png
  96. BIN
      tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_DrawingContext.expected.png
  97. BIN
      tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_DrawingContext.expected.png

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

@ -513,6 +513,7 @@ private:
bool _fullScreenActive;
SystemDecorations _decorations;
AvnWindowState _lastWindowState;
AvnWindowState _actualWindowState;
bool _inSetWindowState;
NSRect _preZoomSize;
bool _transitioningWindowState;
@ -539,6 +540,7 @@ private:
_transitioningWindowState = false;
_inSetWindowState = false;
_lastWindowState = Normal;
_actualWindowState = Normal;
WindowEvents = events;
[Window setCanBecomeKeyAndMain];
[Window disableCursorRects];
@ -633,7 +635,7 @@ private:
void WindowStateChanged () override
{
if(!_inSetWindowState && !_transitioningWindowState)
if(_shown && !_inSetWindowState && !_transitioningWindowState)
{
AvnWindowState state;
GetWindowState(&state);
@ -963,14 +965,14 @@ private:
{
@autoreleasepool
{
if(_lastWindowState == state)
if(_actualWindowState == state)
{
return S_OK;
}
_inSetWindowState = true;
auto currentState = _lastWindowState;
auto currentState = _actualWindowState;
_lastWindowState = state;
if(currentState == Normal)
@ -1049,8 +1051,11 @@ private:
}
break;
}
_actualWindowState = _lastWindowState;
}
_inSetWindowState = false;
return S_OK;

5
samples/ControlCatalog/MainWindow.xaml.cs

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

6
samples/ControlCatalog/Pages/ContextFlyoutPage.axaml

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

3
samples/RenderDemo/MainWindow.xaml

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

25
samples/RenderDemo/Pages/CustomAnimatorPage.xaml

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

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

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

16
samples/RenderDemo/Pages/CustomStringAnimator.cs

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

36
samples/RenderDemo/Pages/TransitionsPage.xaml

@ -141,6 +141,39 @@
<Style Selector="Border.Shadow:pointerover">
<Setter Property="BoxShadow" Value="inset 30 30 20 30 Green, 20 40 20 10 Red"/>
</Style>
<Style Selector="Border.Rect10">
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Background" Duration="0:0:0.5" />
</Transitions>
</Setter>
<Setter Property="Background" Value="Red" />
</Style>
<Style Selector="Border.Rect10:pointerover">
<Setter Property="Background" Value="Orange" />
</Style>
<Style Selector="Border.Rect11">
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Background" Duration="0:0:0.5" />
</Transitions>
</Setter>
<Setter Property="Background" Value="Red" />
</Style>
<Style Selector="Border.Rect11:pointerover">
<Setter Property="Background" >
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0" Color="Red"/>
<GradientStop Offset="1" Color="Blue"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Setter>
</Style>
</Styles>
</UserControl.Styles>
@ -166,6 +199,9 @@
<Border Classes="Test Shadow" CornerRadius="10" Child="{x:Null}" />
<Border Classes="Test Shadow" CornerRadius="0 30 60 0" Child="{x:Null}" />
<Border Classes="Test Rect10" />
<Border Classes="Test Rect11" />
</WrapPanel>
</StackPanel>
</Grid>

29
src/Avalonia.Animation/Animation.cs

@ -195,6 +195,33 @@ namespace Avalonia.Animation
[Content]
public KeyFrames Children { get; } = new KeyFrames();
// Store values for the Animator attached properties for IAnimationSetter objects.
private static readonly Dictionary<IAnimationSetter, Type> s_animators = new Dictionary<IAnimationSetter, Type>();
/// <summary>
/// Gets the value of the Animator attached property for a setter.
/// </summary>
/// <param name="setter">The animation setter.</param>
/// <returns>The property animator type.</returns>
public static Type GetAnimator(IAnimationSetter setter)
{
if (s_animators.TryGetValue(setter, out var type))
{
return type;
}
return null;
}
/// <summary>
/// Sets the value of the Animator attached property for a setter.
/// </summary>
/// <param name="setter">The animation setter.</param>
/// <param name="value">The property animator value.</param>
public static void SetAnimator(IAnimationSetter setter, Type value)
{
s_animators[setter] = value;
}
private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator)> Animators = new List<(Func<AvaloniaProperty, bool>, Type)>
{
( prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator) ),
@ -249,7 +276,7 @@ namespace Avalonia.Animation
{
foreach (var setter in keyframe.Setters)
{
var handler = GetAnimatorType(setter.Property);
var handler = Animation.GetAnimator(setter) ?? GetAnimatorType(setter.Property);
if (handler == null)
{

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

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

24
src/Avalonia.Base/AvaloniaObjectExtensions.cs

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

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

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

3
src/Avalonia.Controls/ApiCompatBaseline.txt

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

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

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

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

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

3
src/Avalonia.Controls/ExperimentalAcrylicBorder.cs

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

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

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

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

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

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

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

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

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

36
src/Avalonia.Controls/ScrollViewer.cs

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

112
src/Avalonia.Controls/TextBox.cs

@ -119,9 +119,9 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<TextBox, bool>(nameof(RevealPassword));
public static readonly DirectProperty<TextBox, bool> CanCutProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanCut),
o => o.CanCut);
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanCut),
o => o.CanCut);
public static readonly DirectProperty<TextBox, bool> CanCopyProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
@ -129,9 +129,21 @@ namespace Avalonia.Controls
o => o.CanCopy);
public static readonly DirectProperty<TextBox, bool> CanPasteProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanPaste),
o => o.CanPaste);
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanPaste),
o => o.CanPaste);
public static readonly StyledProperty<bool> IsUndoEnabledProperty =
AvaloniaProperty.Register<TextBox, bool>(
nameof(IsUndoEnabled),
defaultValue: true);
public static readonly DirectProperty<TextBox, int> UndoLimitProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>(
nameof(UndoLimit),
o => o.UndoLimit,
(o, v) => o.UndoLimit = v,
unsetValue: -1);
struct UndoRedoState : IEquatable<UndoRedoState>
{
@ -218,7 +230,7 @@ namespace Avalonia.Controls
value = CoerceCaretIndex(value);
SetAndRaise(CaretIndexProperty, ref _caretIndex, value);
UndoRedoState state;
if (_undoRedoHelper.TryGetLastState(out state) && state.Text == Text)
if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text)
_undoRedoHelper.UpdateLastState();
}
}
@ -316,7 +328,7 @@ namespace Avalonia.Controls
SelectionEnd = CoerceCaretIndex(SelectionEnd, value);
CaretIndex = CoerceCaretIndex(caretIndex, value);
if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing)
if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing)
{
_undoRedoHelper.Clear();
}
@ -329,7 +341,7 @@ namespace Avalonia.Controls
get { return GetSelection(); }
set
{
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
if (string.IsNullOrEmpty(value))
{
DeleteSelection();
@ -338,7 +350,7 @@ namespace Avalonia.Controls
{
HandleTextInput(value);
}
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
}
}
@ -446,6 +458,36 @@ namespace Avalonia.Controls
private set { SetAndRaise(CanPasteProperty, ref _canPaste, value); }
}
/// <summary>
/// Property for determining whether undo/redo is enabled
/// </summary>
public bool IsUndoEnabled
{
get { return GetValue(IsUndoEnabledProperty); }
set { SetValue(IsUndoEnabledProperty, value); }
}
public int UndoLimit
{
get { return _undoRedoHelper.Limit; }
set
{
if (_undoRedoHelper.Limit != value)
{
// can't use SetAndRaise due to using _undoRedoHelper.Limit
// (can't send a ref of a property to SetAndRaise),
// so use RaisePropertyChanged instead.
var oldValue = _undoRedoHelper.Limit;
_undoRedoHelper.Limit = value;
RaisePropertyChanged(UndoLimitProperty, oldValue, value);
}
// from docs at
// https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled:
// "Setting UndoLimit clears the undo queue."
_undoRedoHelper.Clear();
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
@ -465,6 +507,15 @@ namespace Avalonia.Controls
UpdatePseudoclasses();
UpdateCommandStates();
}
else if (change.Property == IsUndoEnabledProperty && change.NewValue.GetValueOrDefault<bool>() == false)
{
// from docs at
// https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled:
// "Setting this property to false clears the undo stack.
// Therefore, if you disable undo and then re-enable it, undo commands still do not work
// because the undo stack was emptied when you disabled undo."
_undoRedoHelper.Clear();
}
}
private void UpdateCommandStates()
@ -551,7 +602,10 @@ namespace Avalonia.Controls
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
CaretIndex += input.Length;
ClearSelection();
_undoRedoHelper.DiscardRedo();
if (IsUndoEnabled)
{
_undoRedoHelper.DiscardRedo();
}
}
}
@ -570,10 +624,10 @@ namespace Avalonia.Controls
var text = GetSelection();
if (text is null) return;
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
Copy();
DeleteSelection();
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
}
public async void Copy()
@ -591,9 +645,9 @@ namespace Avalonia.Controls
if (text is null) return;
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
HandleTextInput(text);
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
}
protected override void OnKeyDown(KeyEventArgs e)
@ -638,7 +692,7 @@ namespace Avalonia.Controls
Paste();
handled = true;
}
else if (Match(keymap.Undo))
else if (Match(keymap.Undo) && IsUndoEnabled)
{
try
{
@ -652,7 +706,7 @@ namespace Avalonia.Controls
handled = true;
}
else if (Match(keymap.Redo))
else if (Match(keymap.Redo) && IsUndoEnabled)
{
try
{
@ -752,7 +806,7 @@ namespace Avalonia.Controls
break;
case Key.Back:
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{
SetSelectionForControlBackspace();
@ -776,13 +830,13 @@ namespace Avalonia.Controls
CaretIndex -= removedCharacters;
ClearSelection();
}
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
handled = true;
break;
case Key.Delete:
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{
SetSelectionForControlDelete();
@ -804,7 +858,7 @@ namespace Avalonia.Controls
SetTextInternal(text.Substring(0, caretIndex) +
text.Substring(caretIndex + removedCharacters));
}
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
handled = true;
break;
@ -812,9 +866,9 @@ namespace Avalonia.Controls
case Key.Enter:
if (AcceptsReturn)
{
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
HandleTextInput(NewLine);
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
handled = true;
}
@ -823,9 +877,9 @@ namespace Avalonia.Controls
case Key.Tab:
if (AcceptsTab)
{
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
HandleTextInput("\t");
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
handled = true;
}
else
@ -1251,5 +1305,13 @@ namespace Avalonia.Controls
ClearSelection();
}
}
private void SnapshotUndoRedo()
{
if (IsUndoEnabled)
{
_undoRedoHelper.Snapshot();
}
}
}
}

3
src/Avalonia.Controls/TickBar.cs

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

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

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

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

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

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

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

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

@ -27,7 +27,9 @@
<Image>
<DrawingImage>
<GeometryDrawing Brush="{Binding $parent[ToggleButton].Foreground}">
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>
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>
@ -38,7 +40,9 @@
<Image>
<DrawingImage>
<GeometryDrawing Brush="{Binding $parent[ToggleButton].Foreground}">
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>
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>
@ -49,7 +53,9 @@
<Image>
<DrawingImage>
<GeometryDrawing Brush="{Binding $parent[ToggleButton].Foreground}">
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>
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>

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

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

@ -7,12 +7,12 @@ namespace Avalonia.Diagnostics.Converters
{
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);
}
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)
{

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

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

5
src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -18,10 +18,10 @@ namespace Avalonia.Diagnostics.ViewModels
{
private readonly IVisual _control;
private readonly IDictionary<object, List<PropertyViewModel>> _propertyIndex;
private AvaloniaPropertyViewModel _selectedProperty;
private PropertyViewModel? _selectedProperty;
private bool _snapshotStyles;
private bool _showInactiveStyles;
private string _styleStatus;
private string? _styleStatus;
public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control)
{
@ -83,7 +83,8 @@ namespace Avalonia.Diagnostics.ViewModels
{
foreach (var setter in style.Setters)
{
if (setter is Setter regularSetter)
if (setter is Setter regularSetter
&& regularSetter.Property != null)
{
var setterValue = regularSetter.Value;
@ -115,13 +116,14 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
private (object resourceKey, bool isDynamic)? GetResourceInfo(object value)
private (object resourceKey, bool isDynamic)? GetResourceInfo(object? value)
{
if (value is StaticResourceExtension staticResource)
{
return (staticResource.ResourceKey, false);
}
else if (value is DynamicResourceExtension dynamicResource)
else if (value is DynamicResourceExtension dynamicResource
&& dynamicResource.ResourceKey != null)
{
return (dynamicResource.ResourceKey, true);
}
@ -137,7 +139,7 @@ namespace Avalonia.Diagnostics.ViewModels
public ObservableCollection<PseudoClassViewModel> PseudoClasses { get; }
public AvaloniaPropertyViewModel SelectedProperty
public PropertyViewModel? SelectedProperty
{
get => _selectedProperty;
set => RaiseAndSetIfChanged(ref _selectedProperty, value);
@ -155,7 +157,7 @@ namespace Avalonia.Diagnostics.ViewModels
set => RaiseAndSetIfChanged(ref _showInactiveStyles, value);
}
public string StyleStatus
public string? StyleStatus
{
get => _styleStatus;
set => RaiseAndSetIfChanged(ref _styleStatus, value);
@ -248,7 +250,7 @@ namespace Avalonia.Diagnostics.ViewModels
.Select(x => new ClrPropertyViewModel(o, x));
}
private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
private void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (_propertyIndex.TryGetValue(e.Property, out var properties))
{
@ -261,9 +263,10 @@ namespace Avalonia.Diagnostics.ViewModels
Layout.ControlPropertyChanged(sender, e);
}
private void ControlPropertyChanged(object sender, PropertyChangedEventArgs e)
private void ControlPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (_propertyIndex.TryGetValue(e.PropertyName, out var properties))
if (e.PropertyName != null
&& _propertyIndex.TryGetValue(e.PropertyName, out var properties))
{
foreach (var property in properties)
{
@ -277,7 +280,7 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
private void OnClassesChanged(object sender, NotifyCollectionChangedEventArgs e)
private void OnClassesChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (!SnapshotStyles)
{
@ -349,10 +352,10 @@ namespace Avalonia.Diagnostics.ViewModels
{
public static PropertyComparer Instance { get; } = new PropertyComparer();
public int Compare(PropertyViewModel x, PropertyViewModel y)
public int Compare(PropertyViewModel? x, PropertyViewModel? y)
{
var groupX = GroupIndex(x.Group);
var groupY = GroupIndex(y.Group);
var groupX = GroupIndex(x?.Group);
var groupY = GroupIndex(y?.Group);
if (groupX != groupY)
{
@ -360,11 +363,11 @@ namespace Avalonia.Diagnostics.ViewModels
}
else
{
return string.CompareOrdinal(x.Name, y.Name);
return string.CompareOrdinal(x?.Name, y?.Name);
}
}
private int GroupIndex(string group)
private int GroupIndex(string? group)
{
switch (group)
{

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

@ -12,14 +12,14 @@ namespace Avalonia.Diagnostics.ViewModels
private readonly IVisual _control;
private Thickness _borderThickness;
private double _height;
private string _heightConstraint;
private string? _heightConstraint;
private HorizontalAlignment _horizontalAlignment;
private Thickness _marginThickness;
private Thickness _paddingThickness;
private bool _updatingFromControl;
private VerticalAlignment _verticalAlignment;
private double _width;
private string _widthConstraint;
private string? _widthConstraint;
public ControlLayoutViewModel(IVisual control)
{
@ -80,13 +80,13 @@ namespace Avalonia.Diagnostics.ViewModels
private set => RaiseAndSetIfChanged(ref _height, value);
}
public string WidthConstraint
public string? WidthConstraint
{
get => _widthConstraint;
private set => RaiseAndSetIfChanged(ref _widthConstraint, value);
}
public string HeightConstraint
public string? HeightConstraint
{
get => _heightConstraint;
private set => RaiseAndSetIfChanged(ref _heightConstraint, value);
@ -112,7 +112,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
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 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
{

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

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

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

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

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

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

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

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Diagnostics.Models;
@ -23,8 +22,8 @@ namespace Avalonia.Diagnostics.ViewModels
};
private readonly MainViewModel _mainViewModel;
private FiredEvent _selectedEvent;
private EventTreeNodeBase _selectedNode;
private FiredEvent? _selectedEvent;
private EventTreeNodeBase? _selectedNode;
public EventsPageViewModel(MainViewModel mainViewModel)
{
@ -48,13 +47,13 @@ namespace Avalonia.Diagnostics.ViewModels
public ObservableCollection<FiredEvent> RecordedEvents { get; } = new ObservableCollection<FiredEvent>();
public FiredEvent SelectedEvent
public FiredEvent? SelectedEvent
{
get => _selectedEvent;
set => RaiseAndSetIfChanged(ref _selectedEvent, value);
}
public EventTreeNodeBase SelectedNode
public EventTreeNodeBase? SelectedNode
{
get => _selectedNode;
set => RaiseAndSetIfChanged(ref _selectedNode, value);
@ -99,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)
{

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

@ -11,10 +11,13 @@ namespace Avalonia.Diagnostics.ViewModels
private readonly Dictionary<string, string> _errors = new Dictionary<string, string>();
private string _filterString = string.Empty;
private bool _useRegexFilter, _useCaseSensitiveFilter, _useWholeWordFilter;
private string _processedFilter;
private Regex _filterRegex;
private Regex? _filterRegex;
public event EventHandler RefreshFilter;
public event EventHandler? RefreshFilter;
public bool HasErrors => _errors.Count > 0;
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
public bool Filter(string input)
{
@ -31,13 +34,11 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
_processedFilter = FilterString.Trim();
try
{
var options = RegexOptions.Compiled;
var pattern = UseRegexFilter
? _processedFilter : Regex.Escape(_processedFilter);
? FilterString.Trim() : Regex.Escape(FilterString.Trim());
if (!UseCaseSensitiveFilter)
{
options |= RegexOptions.IgnoreCase;
@ -109,16 +110,13 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public IEnumerable GetErrors(string propertyName)
public IEnumerable GetErrors(string? propertyName)
{
if (_errors.TryGetValue(propertyName, out var error))
if (propertyName != null
&& _errors.TryGetValue(propertyName, out var error))
{
yield return error;
}
}
public bool HasErrors => _errors.Count > 0;
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
}
}

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

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

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

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

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

@ -1,10 +1,10 @@
using System;
using System.ComponentModel;
using Avalonia.Controls;
using Avalonia.Diagnostics.Models;
using Avalonia.Input;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
@ -17,13 +17,16 @@ namespace Avalonia.Diagnostics.ViewModels
private readonly IDisposable _pointerOverSubscription;
private ViewModelBase _content;
private int _selectedTab;
private string _focusedControl;
private string _pointerOverElement;
private string? _focusedControl;
private string? _pointerOverElement;
private bool _shouldVisualizeMarginPadding = true;
private bool _shouldVisualizeDirtyRects;
private bool _showFpsOverlay;
#nullable disable
// Remove "nullable disable" after MemberNotNull will work on our CI.
public MainViewModel(TopLevel root)
#nullable restore
{
_root = root;
_logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root));
@ -84,6 +87,7 @@ namespace Avalonia.Diagnostics.ViewModels
public ViewModelBase Content
{
get { return _content; }
// [MemberNotNull(nameof(_content))]
private set
{
if (_content is TreePageViewModel oldTree &&
@ -114,39 +118,40 @@ namespace Avalonia.Diagnostics.ViewModels
public int SelectedTab
{
get { return _selectedTab; }
// [MemberNotNull(nameof(_content))]
set
{
_selectedTab = value;
switch (value)
{
case 0:
Content = _logicalTree;
break;
case 1:
Content = _visualTree;
break;
case 2:
Content = _events;
break;
default:
Content = _logicalTree;
break;
}
RaisePropertyChanged();
}
}
public string FocusedControl
public string? FocusedControl
{
get { return _focusedControl; }
private set { RaiseAndSetIfChanged(ref _focusedControl, value); }
}
public string PointerOverElement
public string? PointerOverElement
{
get { return _pointerOverElement; }
private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); }
}
private void UpdateConsoleContext(ConsoleContext context)
{
context.root = _root;
@ -187,7 +192,7 @@ namespace Avalonia.Diagnostics.ViewModels
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))
{
@ -208,5 +213,12 @@ namespace Avalonia.Diagnostics.ViewModels
tree.SelectControl(control);
}
}
public int? StartupScreenIndex { get; private set; } = default;
public void SetOptions(DevToolsOptions options)
{
StartupScreenIndex = options.StartupScreenIndex;
}
}
}

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

@ -16,9 +16,11 @@ namespace Avalonia.Diagnostics.ViewModels
public abstract string Group { get; }
public abstract string Type { get; }
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)
{
@ -31,13 +33,13 @@ namespace Avalonia.Diagnostics.ViewModels
if (!converter.CanConvertTo(typeof(string)) ||
converter.GetType() == typeof(CollectionConverter))
{
return value.ToString();
return value.ToString() ?? "(null)";
}
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);
@ -56,7 +58,7 @@ namespace Avalonia.Diagnostics.ViewModels
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);

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

@ -8,7 +8,7 @@ namespace Avalonia.Diagnostics.ViewModels
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;
Tint = isDynamic ? Brushes.Orange : Brushes.Brown;
@ -16,12 +16,14 @@ namespace Avalonia.Diagnostics.ViewModels
public void CopyResourceKey()
{
if (Key is null)
var textToCopy = Key?.ToString();
if (textToCopy is null)
{
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 object Value { get; }
public object? Value { get; }
public bool IsActive
{
@ -25,7 +25,7 @@ namespace Avalonia.Diagnostics.ViewModels
set => RaiseAndSetIfChanged(ref _isVisible, value);
}
public SetterViewModel(AvaloniaProperty property, object value)
public SetterViewModel(AvaloniaProperty property, object? value)
{
Property = property;
Name = property.Name;
@ -36,12 +36,14 @@ namespace Avalonia.Diagnostics.ViewModels
public void CopyValue()
{
if (Value is null)
var textToCopy = Value?.ToString();
if (textToCopy is null)
{
return;
}
CopyToClipboard(Value.ToString());
CopyToClipboard(textToCopy);
}
public void CopyPropertyName()

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

@ -9,17 +9,18 @@ using Avalonia.VisualTree;
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 bool _isExpanded;
public TreeNode(IVisual visual, TreeNode parent)
public TreeNode(IVisual visual, TreeNode? parent)
{
Parent = parent;
Type = visual.GetType().Name;
Visual = visual;
_classes = string.Empty;
if (visual is IControl control)
{
@ -51,10 +52,9 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public TreeNodeCollection Children
public abstract TreeNodeCollection Children
{
get;
protected set;
}
public string Classes
@ -63,7 +63,7 @@ namespace Avalonia.Diagnostics.ViewModels
private set { RaiseAndSetIfChanged(ref _classes, value); }
}
public string ElementName
public string? ElementName
{
get;
}
@ -79,7 +79,7 @@ namespace Avalonia.Diagnostics.ViewModels
set { RaiseAndSetIfChanged(ref _isExpanded, value); }
}
public TreeNode Parent
public TreeNode? Parent
{
get;
}
@ -92,7 +92,7 @@ namespace Avalonia.Diagnostics.ViewModels
public void Dispose()
{
_classesSubscription.Dispose();
_classesSubscription?.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.Specialized;
using System.ComponentModel;
using Avalonia.Collections;
namespace Avalonia.Diagnostics.ViewModels
{
internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList<TreeNode>, IDisposable
{
private AvaloniaList<TreeNode> _inner;
private AvaloniaList<TreeNode>? _inner;
public TreeNodeCollection(TreeNode owner) => Owner = owner;
public TreeNode this[int index]
{
get
{
EnsureInitialized();
return _inner[index];
}
}
public TreeNode this[int index] => EnsureInitialized()[index];
public int Count
{
get
{
EnsureInitialized();
return _inner.Count;
}
}
public int Count => EnsureInitialized().Count;
protected TreeNode Owner { get; }
public event NotifyCollectionChangedEventHandler CollectionChanged
public event NotifyCollectionChangedEventHandler? CollectionChanged
{
add => _inner.CollectionChanged += value;
remove => _inner.CollectionChanged -= value;
add => EnsureInitialized().CollectionChanged += value;
remove => EnsureInitialized().CollectionChanged -= value;
}
public event PropertyChangedEventHandler PropertyChanged
public event PropertyChangedEventHandler? PropertyChanged
{
add => _inner.PropertyChanged += value;
remove => _inner.PropertyChanged -= value;
add => EnsureInitialized().PropertyChanged += value;
remove => EnsureInitialized().PropertyChanged -= value;
}
public virtual void Dispose()
@ -58,21 +45,21 @@ namespace Avalonia.Diagnostics.ViewModels
public IEnumerator<TreeNode> GetEnumerator()
{
EnsureInitialized();
return _inner.GetEnumerator();
return EnsureInitialized().GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
protected abstract void Initialize(AvaloniaList<TreeNode> nodes);
private void EnsureInitialized()
private AvaloniaList<TreeNode> EnsureInitialized()
{
if (_inner is null)
{
_inner = new AvaloniaList<TreeNode>();
Initialize(_inner);
}
return _inner;
}
}
}

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

@ -6,8 +6,8 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class TreePageViewModel : ViewModelBase, IDisposable
{
private TreeNode _selectedNode;
private ControlDetailsViewModel _details;
private TreeNode? _selectedNode;
private ControlDetailsViewModel? _details;
public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes)
{
@ -29,7 +29,7 @@ namespace Avalonia.Diagnostics.ViewModels
public TreeNode[] Nodes { get; protected set; }
public TreeNode SelectedNode
public TreeNode? SelectedNode
{
get => _selectedNode;
private set
@ -44,7 +44,7 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public ControlDetailsViewModel Details
public ControlDetailsViewModel? Details
{
get => _details;
private set
@ -68,7 +68,7 @@ namespace Avalonia.Diagnostics.ViewModels
_details?.Dispose();
}
public TreeNode FindNode(IControl control)
public TreeNode? FindNode(IControl control)
{
foreach (var node in Nodes)
{
@ -104,7 +104,7 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
private void ExpandNode(TreeNode node)
private void ExpandNode(TreeNode? node)
{
if (node != null)
{
@ -113,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)
{

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.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace Avalonia.Diagnostics.ViewModels
{
internal class ViewModelBase : INotifyPropertyChanged
{
private PropertyChangedEventHandler _propertyChanged;
private PropertyChangedEventHandler? _propertyChanged;
private List<string> events = new List<string>();
public event PropertyChangedEventHandler PropertyChanged
public event PropertyChangedEventHandler? PropertyChanged
{
add { _propertyChanged += value; events.Add("added"); }
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))
{
@ -32,7 +32,7 @@ namespace Avalonia.Diagnostics.ViewModels
return false;
}
protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
protected void RaisePropertyChanged([CallerMemberName] string propertyName = null!)
{
var e = new PropertyChangedEventArgs(propertyName);
OnPropertyChanged(e);

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

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

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

@ -30,22 +30,26 @@ namespace Avalonia.Diagnostics.Views
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);
}
}
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)
{
case Key.Enter:
vm.Execute();
_ = vm.Execute();
e.Handled = true;
break;
case Key.Up:

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)
{

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

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

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

@ -15,7 +15,7 @@ namespace Avalonia.Diagnostics.Views
internal class MainWindow : Window, IStyleHost
{
private readonly IDisposable _keySubscription;
private TopLevel _root;
private TopLevel? _root;
public MainWindow()
{
@ -24,9 +24,26 @@ namespace Avalonia.Diagnostics.Views
_keySubscription = InputManager.Instance.Process
.OfType<RawKeyEventArgs>()
.Subscribe(RawKeyDown);
EventHandler? lh = default;
lh = (s, e) =>
{
this.Opened -= lh;
if ((DataContext as MainViewModel)?.StartupScreenIndex is int index)
{
var screens = this.Screens;
if (index > -1 && index < screens.ScreenCount)
{
var screen = screens.All[index];
this.Position = screen.Bounds.TopLeft;
this.WindowState = WindowState.Maximized;
}
}
};
this.Opened += lh;
}
public TopLevel Root
public TopLevel? Root
{
get => _root;
set
@ -43,7 +60,7 @@ namespace Avalonia.Diagnostics.Views
if (_root != null)
{
_root.Closed += RootClosed;
DataContext = new MainViewModel(value);
DataContext = new MainViewModel(_root);
}
else
{
@ -53,15 +70,20 @@ namespace Avalonia.Diagnostics.Views
}
}
IStyleHost IStyleHost.StylingParent => null;
IStyleHost? IStyleHost.StylingParent => null;
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
_keySubscription.Dispose();
_root.Closed -= RootClosed;
_root = null;
((MainViewModel)DataContext)?.Dispose();
if (_root != null)
{
_root.Closed -= RootClosed;
_root = null;
}
((MainViewModel?)DataContext)?.Dispose();
}
private void InitializeComponent()
@ -71,12 +93,20 @@ namespace Avalonia.Diagnostics.Views
private void RawKeyDown(RawKeyEventArgs e)
{
var vm = (MainViewModel?)DataContext;
if (vm is null)
{
return;
}
const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift;
if (e.Modifiers == modifiers)
{
#pragma warning disable CS0618 // Type or member is obsolete
var point = (Root as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default;
#pragma warning restore CS0618 // Type or member is obsolete
var control = Root.GetVisualsAt(point, x =>
{
if (x is AdornerLayer || !x.IsVisible) return false;
@ -87,7 +117,6 @@ namespace Avalonia.Diagnostics.Views
if (control != null)
{
var vm = (MainViewModel)DataContext;
vm.SelectControl((IControl)control);
}
}
@ -97,12 +126,14 @@ namespace Avalonia.Diagnostics.Views
{
var enable = e.Key == Key.S;
var vm = (MainViewModel)DataContext;
vm.EnableSnapshotStyles(enable);
}
}
}
private void RootClosed(object sender, EventArgs e) => Close();
private void RootClosed(object? sender, EventArgs e) => Close();
public void SetOptions(DevToolsOptions options) =>
(DataContext as MainViewModel)?.SetOptions(options);
}
}

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

@ -14,12 +14,13 @@ namespace Avalonia.Diagnostics.Views
internal class TreePageView : UserControl
{
private readonly Panel _adorner;
private AdornerLayer _currentLayer;
private AdornerLayer? _currentLayer;
private TreeView _tree;
public TreePageView()
{
InitializeComponent();
_tree = this.FindControl<TreeView>("tree");
_tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized;
_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;
_currentLayer = AdornerLayer.GetAdornerLayer(visual);
@ -53,8 +60,6 @@ namespace Avalonia.Diagnostics.Views
_currentLayer.Children.Add(_adorner);
AdornerLayer.SetAdornedElement(_adorner, visual);
var vm = (TreePageViewModel) DataContext;
if (vm.MainView.ShouldVisualizeMarginPadding)
{
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);
}
protected void RemoveAdorner(object sender, PointerEventArgs e)
protected void RemoveAdorner(object? sender, PointerEventArgs e)
{
foreach (var border in _adorner.Children.OfType<Border>())
{
@ -90,18 +95,17 @@ namespace Avalonia.Diagnostics.Views
private void InitializeComponent()
{
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;
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.
// 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)
{
Control control = visual as Control;
Control? control = visual as Control;
builder.Append(Indent(indent - 1));

23
src/Avalonia.Input/KeyboardDevice.cs

@ -217,12 +217,31 @@ namespace Avalonia.Input
{
var bindings = (currentHandler as IInputElement)?.KeyBindings;
if (bindings != null)
{
KeyBinding[]? bindingsCopy = null;
// Create a copy of the KeyBindings list if there's a binding which matches the event.
// If we don't do this the foreach loop will throw an InvalidOperationException when the KeyBindings list is changed.
// This can happen when a new view is loaded which adds its own KeyBindings to the handler.
foreach (var binding in bindings)
{
if (ev.Handled)
if (binding.Gesture?.Matches(ev) == true)
{
bindingsCopy = bindings.ToArray();
break;
binding.TryHandle(ev);
}
}
if (bindingsCopy is object)
{
foreach (var binding in bindingsCopy)
{
if (ev.Handled)
break;
binding.TryHandle(ev);
}
}
}
currentHandler = currentHandler.VisualParent;
}

2
src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs

@ -33,6 +33,6 @@ namespace Avalonia.Styling.Activators
void IObserver<object>.OnCompleted() { }
void IObserver<object>.OnError(Exception error) { }
void IObserver<object>.OnNext(object value) => PublishNext(Equals(value, _value));
void IObserver<object>.OnNext(object value) => PublishNext(PropertyEqualsSelector.Compare(_property.PropertyType, value, _value));
}
}

32
src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs

@ -1,4 +1,6 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Text;
using Avalonia.Styling.Activators;
@ -75,11 +77,37 @@ namespace Avalonia.Styling
}
else
{
var result = (control.GetValue(_property) ?? string.Empty).Equals(_value);
return result ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance;
return Compare(_property.PropertyType, control.GetValue(_property), _value)
? SelectorMatch.AlwaysThisInstance
: SelectorMatch.NeverThisInstance;
}
}
protected override Selector? MovePrevious() => _previous;
internal static bool Compare(Type propertyType, object propertyValue, object? value)
{
if (propertyType == typeof(object) &&
propertyValue?.GetType() is Type inferredType)
{
propertyType = inferredType;
}
var valueType = value?.GetType();
if (valueType is null || propertyType.IsAssignableFrom(valueType))
{
return Equals(propertyValue, value);
}
var converter = TypeDescriptor.GetConverter(propertyType);
if (converter?.CanConvertFrom(valueType) == true)
{
return Equals(propertyValue, converter.ConvertFrom(null, CultureInfo.InvariantCulture, value));
}
return false;
}
}
}

3
src/Avalonia.Themes.Default/ListBox.xaml

@ -13,7 +13,8 @@
<ScrollViewer Name="PART_ScrollViewer"
Background="{TemplateBinding Background}"
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"

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

@ -57,7 +57,8 @@
<ContentPresenter Grid.Column="0" Grid.ColumnSpan="1" Content="{TemplateBinding InnerLeftContent}"/>
<ScrollViewer Grid.Column="1" Grid.ColumnSpan="1"
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
<Panel>
<TextBlock Name="watermark"
Opacity="0.5"

3
src/Avalonia.Themes.Default/TreeView.xaml

@ -11,7 +11,8 @@
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer Background="{TemplateBinding Background}"
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"

6
src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml

@ -112,9 +112,7 @@
<Panel Height="12"
Width="12" />
<Path x:Name="DropDownGlyph"
Stretch="Uniform"
VerticalAlignment="Center"
Data="M1939 486L2029 576L1024 1581L19 576L109 486L1024 1401L1939 486Z" />
VerticalAlignment="Center" />
</Panel>
</Viewbox>
<Popup Name="PART_Popup"
@ -160,6 +158,8 @@
<Style Selector="ComboBox /template/ Path#DropDownGlyph">
<Setter Property="Fill" Value="{DynamicResource ComboBoxDropDownGlyphForeground}" />
<Setter Property="Data" Value="M1939 486L2029 576L1024 1581L19 576L109 486L1024 1401L1939 486Z" />
<Setter Property="Stretch" Value="Uniform" />
</Style>
<!-- PointerOver State -->

3
src/Avalonia.Themes.Fluent/Controls/ListBox.xaml

@ -24,7 +24,8 @@
<ScrollViewer Name="PART_ScrollViewer"
Background="{TemplateBinding Background}"
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"

3
src/Avalonia.Themes.Fluent/Controls/TextBox.xaml

@ -65,7 +65,8 @@
Text="{TemplateBinding Watermark}"
DockPanel.Dock="Top" />
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
<Panel>
<TextBlock Name="PART_Watermark"
Opacity="0.5"

3
src/Avalonia.Themes.Fluent/Controls/TreeView.xaml

@ -12,7 +12,8 @@
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer Background="{TemplateBinding Background}"
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"

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

3
src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Avalonia.Media.Immutable
{
@ -44,6 +45,8 @@ namespace Avalonia.Media.Immutable
PenLineJoin lineJoin = PenLineJoin.Miter,
double miterLimit = 10.0)
{
Debug.Assert(!(brush is IMutableBrush));
Brush = brush;
Thickness = thickness;
LineCap = lineCap;

61
src/Avalonia.Visuals/Media/PolyLineSegment.cs

@ -0,0 +1,61 @@
using System.Collections.Generic;
using Avalonia.Collections;
namespace Avalonia.Media
{
/// <summary>
/// Represents a set of line segments defined by a points collection with each Point specifying the end point of a line segment.
/// </summary>
public sealed class PolyLineSegment : PathSegment
{
/// <summary>
/// Defines the <see cref="Points"/> property.
/// </summary>
public static readonly StyledProperty<Points> PointsProperty
= AvaloniaProperty.Register<PolyLineSegment, Points>(nameof(Points));
/// <summary>
/// Gets or sets the points.
/// </summary>
/// <value>
/// The points.
/// </value>
public AvaloniaList<Point> Points
{
get => GetValue(PointsProperty);
set => SetValue(PointsProperty, value);
}
/// <summary>
/// Initializes a new instance of the <see cref="PolyLineSegment"/> class.
/// </summary>
public PolyLineSegment()
{
Points = new Points();
}
/// <summary>
/// Initializes a new instance of the <see cref="PolyLineSegment"/> class.
/// </summary>
/// <param name="points">The points.</param>
public PolyLineSegment(IEnumerable<Point> points) : this()
{
Points.AddRange(points);
}
protected internal override void ApplyTo(StreamGeometryContext ctx)
{
var points = Points;
if (points.Count > 0)
{
for (int i = 0; i < points.Count; i++)
{
ctx.LineTo(points[i]);
}
}
}
public override string ToString()
=> Points.Count >= 1 ? "L " + string.Join(" ", Points) : "";
}
}

5
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@ -132,7 +132,10 @@ public static class LinuxFramebufferPlatformExtensions
{
public static int StartLinuxFbDev<T>(this T builder, string[] args, string fbdev = null, double scaling = 1)
where T : AppBuilderBase<T>, new() =>
StartLinuxDirect(builder, args, new FbdevOutput(fbdev) {Scaling = scaling});
StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: null) { Scaling = scaling });
public static int StartLinuxFbDev<T>(this T builder, string[] args, string fbdev, PixelFormat? format, double scaling)
where T : AppBuilderBase<T>, new() =>
StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: format) { Scaling = scaling });
public static int StartLinuxDrm<T>(this T builder, string[] args, string card = null, double scaling = 1)
where T : AppBuilderBase<T>, new() => StartLinuxDirect(builder, args, new DrmOutput(card) {Scaling = scaling});

2
src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs

@ -41,6 +41,6 @@ namespace Avalonia.LinuxFramebuffer
public PixelSize Size => new PixelSize((int)_varInfo.xres, (int) _varInfo.yres);
public int RowBytes => (int) _fixedInfo.line_length;
public Vector Dpi { get; }
public PixelFormat Format => _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888;
public PixelFormat Format => _varInfo.bits_per_pixel == 16 ? PixelFormat.Rgb565 : _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888;
}
}

88
src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs

@ -16,16 +16,33 @@ namespace Avalonia.LinuxFramebuffer
private IntPtr _mappedAddress;
public double Scaling { get; set; }
public FbdevOutput(string fileName = null)
/// <summary>
/// Create a Linux frame buffer device output
/// </summary>
/// <param name="fileName">The frame buffer device name.
/// Defaults to the value in environment variable FRAMEBUFFER or /dev/fb0 when FRAMEBUFFER is not set</param>
public FbdevOutput(string fileName = null) : this(null, null)
{
fileName = fileName ?? Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0";
}
/// <summary>
/// Create a Linux frame buffer device output
/// </summary>
/// <param name="fileName">The frame buffer device name.
/// Defaults to the value in environment variable FRAMEBUFFER or /dev/fb0 when FRAMEBUFFER is not set</param>
/// <param name="format">The required pixel format for the frame buffer.
/// A null value will leave the frame buffer in the current pixel format.
/// Otherwise sets the frame buffer to the required format</param>
public FbdevOutput(string fileName, PixelFormat? format)
{
fileName ??= Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0";
_fd = NativeUnsafeMethods.open(fileName, 2, 0);
if (_fd <= 0)
throw new Exception("Error: " + Marshal.GetLastWin32Error());
try
{
Init();
Init(format);
}
catch
{
@ -34,25 +51,28 @@ namespace Avalonia.LinuxFramebuffer
}
}
void Init()
void Init(PixelFormat? format)
{
fixed (void* pnfo = &_varInfo)
{
if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, pnfo))
throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error());
SetBpp();
if (format.HasValue)
{
SetBpp(format.Value);
if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo))
_varInfo.transp = new fb_bitfield();
if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo))
_varInfo.transp = new fb_bitfield();
NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo);
NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo);
if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, pnfo))
throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error());
if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, pnfo))
throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error());
if (_varInfo.bits_per_pixel != 32)
throw new Exception("Unable to set 32-bit display mode");
if (_varInfo.bits_per_pixel != 32)
throw new Exception("Unable to set 32-bit display mode");
}
}
fixed(void*pnfo = &_fixedInfo)
if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_FSCREENINFO, pnfo))
@ -70,17 +90,43 @@ namespace Avalonia.LinuxFramebuffer
}
}
void SetBpp()
void SetBpp(PixelFormat format)
{
_varInfo.bits_per_pixel = 32;
_varInfo.grayscale = 0;
_varInfo.red = _varInfo.blue = _varInfo.green = _varInfo.transp = new fb_bitfield
switch (format)
{
length = 8
};
_varInfo.green.offset = 8;
_varInfo.blue.offset = 16;
_varInfo.transp.offset = 24;
case PixelFormat.Rgba8888:
_varInfo.bits_per_pixel = 32;
_varInfo.grayscale = 0;
_varInfo.red = _varInfo.blue = _varInfo.green = _varInfo.transp = new fb_bitfield
{
length = 8
};
_varInfo.green.offset = 8;
_varInfo.blue.offset = 16;
_varInfo.transp.offset = 24;
break;
case PixelFormat.Bgra8888:
_varInfo.bits_per_pixel = 32;
_varInfo.grayscale = 0;
_varInfo.red = _varInfo.blue = _varInfo.green = _varInfo.transp = new fb_bitfield
{
length = 8
};
_varInfo.green.offset = 8;
_varInfo.red.offset = 16;
_varInfo.transp.offset = 24;
break;
case PixelFormat.Rgb565:
_varInfo.bits_per_pixel = 16;
_varInfo.grayscale = 0;
_varInfo.red = _varInfo.blue = _varInfo.green = _varInfo.transp = new fb_bitfield();
_varInfo.red.length = 5;
_varInfo.green.offset = 5;
_varInfo.green.length = 6;
_varInfo.blue.offset = 11;
_varInfo.blue.length = 5;
break;
}
}
public string Id { get; private set; }

47
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -164,7 +164,7 @@ namespace Avalonia.Skia
/// <inheritdoc />
public void DrawLine(IPen pen, Point p1, Point p2)
{
using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
using (var paint = CreatePaint(_strokePaint, pen, new Rect(p1, p2).Normalize()))
{
if (paint.Paint is object)
{
@ -177,10 +177,10 @@ namespace Avalonia.Skia
public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
{
var impl = (GeometryImpl) geometry;
var size = geometry.Bounds.Size;
var rect = geometry.Bounds;
using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper))
using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper))
using (var fill = brush != null ? CreatePaint(_fillPaint, brush, rect) : default(PaintWrapper))
using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, rect) : default(PaintWrapper))
{
if (fill.Paint != null)
{
@ -354,7 +354,7 @@ namespace Avalonia.Skia
if (brush != null)
{
using (var paint = CreatePaint(_fillPaint, brush, rect.Rect.Size))
using (var paint = CreatePaint(_fillPaint, brush, rect.Rect))
{
if (isRounded)
{
@ -397,7 +397,7 @@ namespace Avalonia.Skia
if (pen?.Brush != null)
{
using (var paint = CreatePaint(_strokePaint, pen, rect.Rect.Size))
using (var paint = CreatePaint(_strokePaint, pen, rect.Rect))
{
if (paint.Paint is object)
{
@ -417,7 +417,7 @@ namespace Avalonia.Skia
/// <inheritdoc />
public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
{
using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds.Size))
using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds))
{
var textImpl = (FormattedTextImpl) text;
textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering);
@ -427,7 +427,7 @@ namespace Avalonia.Skia
/// <inheritdoc />
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
{
using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Size))
using (var paintWrapper = CreatePaint(_fillPaint, foreground, new Rect(glyphRun.Size)))
{
var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
@ -537,7 +537,7 @@ namespace Avalonia.Skia
var paint = new SKPaint();
Canvas.SaveLayer(paint);
_maskStack.Push(CreatePaint(paint, mask, bounds.Size, true));
_maskStack.Push(CreatePaint(paint, mask, bounds, true));
}
/// <inheritdoc />
@ -593,18 +593,19 @@ namespace Avalonia.Skia
/// <param name="paintWrapper">Paint wrapper.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="gradientBrush">Gradient brush.</param>
private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush)
private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Rect targetRect, IGradientBrush gradientBrush)
{
var tileMode = gradientBrush.SpreadMethod.ToSKShaderTileMode();
var stopColors = gradientBrush.GradientStops.Select(s => s.Color.ToSKColor()).ToArray();
var stopOffsets = gradientBrush.GradientStops.Select(s => (float)s.Offset).ToArray();
var position = targetRect.Position.ToSKPoint();
switch (gradientBrush)
{
case ILinearGradientBrush linearGradient:
{
var start = linearGradient.StartPoint.ToPixels(targetSize).ToSKPoint();
var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint();
var start = position + linearGradient.StartPoint.ToPixels(targetRect.Size).ToSKPoint();
var end = position + linearGradient.EndPoint.ToPixels(targetRect.Size).ToSKPoint();
// would be nice to cache these shaders possibly?
using (var shader =
@ -617,10 +618,10 @@ namespace Avalonia.Skia
}
case IRadialGradientBrush radialGradient:
{
var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint();
var radius = (float)(radialGradient.Radius * targetSize.Width);
var center = position + radialGradient.Center.ToPixels(targetRect.Size).ToSKPoint();
var radius = (float)(radialGradient.Radius * targetRect.Width);
var origin = radialGradient.GradientOrigin.ToPixels(targetSize).ToSKPoint();
var origin = position + radialGradient.GradientOrigin.ToPixels(targetRect.Size).ToSKPoint();
if (origin.Equals(center))
{
@ -665,7 +666,7 @@ namespace Avalonia.Skia
}
case IConicGradientBrush conicGradient:
{
var center = conicGradient.Center.ToPixels(targetSize).ToSKPoint();
var center = position + conicGradient.Center.ToPixels(targetRect.Size).ToSKPoint();
// Skia's default is that angle 0 is from the right hand side of the center point
// but we are matching CSS where the vertical point above the center is 0.
@ -867,10 +868,10 @@ namespace Avalonia.Skia
/// </summary>
/// <param name="paint">The paint to wrap.</param>
/// <param name="brush">Source brush.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="targetRect">Target rect.</param>
/// <param name="disposePaint">Optional dispose of the supplied paint.</param>
/// <returns>Paint wrapper for given brush.</returns>
internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize, bool disposePaint = false)
internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Rect targetRect, bool disposePaint = false)
{
var paintWrapper = new PaintWrapper(paint, disposePaint);
@ -889,7 +890,7 @@ namespace Avalonia.Skia
if (brush is IGradientBrush gradient)
{
ConfigureGradientBrush(ref paintWrapper, targetSize, gradient);
ConfigureGradientBrush(ref paintWrapper, targetRect, gradient);
return paintWrapper;
}
@ -909,7 +910,7 @@ namespace Avalonia.Skia
if (tileBrush != null && tileBrushImage != null)
{
ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage);
ConfigureTileBrush(ref paintWrapper, targetRect.Size, tileBrush, tileBrushImage);
}
else
{
@ -924,10 +925,10 @@ namespace Avalonia.Skia
/// </summary>
/// <param name="paint">The paint to wrap.</param>
/// <param name="pen">Source pen.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="targetRect">Target rect.</param>
/// <param name="disposePaint">Optional dispose of the supplied paint.</param>
/// <returns></returns>
private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Size targetSize, bool disposePaint = false)
private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Rect targetRect, bool disposePaint = false)
{
// In Skia 0 thickness means - use hairline rendering
// and for us it means - there is nothing rendered.
@ -936,7 +937,7 @@ namespace Avalonia.Skia
return default;
}
var rv = CreatePaint(paint, pen.Brush, targetSize, disposePaint);
var rv = CreatePaint(paint, pen.Brush, targetRect, disposePaint);
paint.IsStroke = true;
paint.StrokeWidth = (float) pen.Thickness;

13
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Avalonia.Media;
using Avalonia.Platform;
@ -175,7 +176,8 @@ namespace Avalonia.Skia
foreach (var line in _skiaLines.Where(l =>
(l.Start + l.Length) > index &&
lastIndex >= l.Start))
lastIndex >= l.Start &&
!l.IsEmptyTrailingLine))
{
int lineEndIndex = line.Start + (line.Length > 0 ? line.Length - 1 : 0);
@ -276,9 +278,9 @@ namespace Avalonia.Skia
if (fb != null)
{
//TODO: figure out how to get the brush size
//TODO: figure out how to get the brush rect
currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb,
new Size());
default);
}
else
{
@ -466,7 +468,8 @@ namespace Avalonia.Skia
for (int i = line.Start; i < line.Start + line.TextLength; i++)
{
float w = _paint.MeasureText(Text[i].ToString());
var c = Text[i];
var w = line.IsEmptyTrailingLine ? 0 :_paint.MeasureText(Text[i].ToString());
_rects.Add(new Rect(
prevRight,
@ -611,6 +614,7 @@ namespace Avalonia.Skia
lastLine.Width = lastLineWidth;
lastLine.Height = _lineHeight;
lastLine.Top = curY;
lastLine.IsEmptyTrailingLine = true;
_skiaLines.Add(lastLine);
@ -713,6 +717,7 @@ namespace Avalonia.Skia
public int TextLength;
public float Top;
public float Width;
public bool IsEmptyTrailingLine;
};
private struct FBrushRange

4
src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs

@ -34,10 +34,10 @@ namespace Avalonia.Direct2D1.Media
{
var wrapper = clientDrawingEffect as BrushWrapper;
// TODO: Work out how to get the size below rather than passing new Size().
// TODO: Work out how to get the rect below rather than passing default.
var brush = (wrapper == null) ?
_foreground :
_context.CreateBrush(wrapper.Brush, new Size()).PlatformBrush;
_context.CreateBrush(wrapper.Brush, default).PlatformBrush;
_renderTarget.DrawGlyphRun(
new RawVector2 { X = baselineOriginX, Y = baselineOriginY },

32
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -192,7 +192,7 @@ namespace Avalonia.Direct2D1.Media
{
using (var d2dSource = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext))
using (var sourceBrush = new BitmapBrush(_deviceContext, d2dSource.Value))
using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect.Size))
using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect))
using (var geometry = new SharpDX.Direct2D1.RectangleGeometry(Direct2D1Platform.Direct2D1Factory, destRect.ToDirect2D()))
{
if (d2dOpacityMask.PlatformBrush != null)
@ -217,9 +217,7 @@ namespace Avalonia.Direct2D1.Media
{
if (pen != null)
{
var size = new Rect(p1, p2).Size;
using (var d2dBrush = CreateBrush(pen.Brush, size))
using (var d2dBrush = CreateBrush(pen.Brush, new Rect(p1, p2).Normalize()))
using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext))
{
if (d2dBrush.PlatformBrush != null)
@ -245,7 +243,7 @@ namespace Avalonia.Direct2D1.Media
{
if (brush != null)
{
using (var d2dBrush = CreateBrush(brush, geometry.Bounds.Size))
using (var d2dBrush = CreateBrush(brush, geometry.Bounds))
{
if (d2dBrush.PlatformBrush != null)
{
@ -257,7 +255,7 @@ namespace Avalonia.Direct2D1.Media
if (pen != null)
{
using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen).Size))
using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen)))
using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext))
{
if (d2dBrush.PlatformBrush != null)
@ -282,7 +280,7 @@ namespace Avalonia.Direct2D1.Media
if (brush != null)
{
using (var b = CreateBrush(brush, rect.Size))
using (var b = CreateBrush(brush, rect))
{
if (b.PlatformBrush != null)
{
@ -311,7 +309,7 @@ namespace Avalonia.Direct2D1.Media
if (pen?.Brush != null)
{
using (var wrapper = CreateBrush(pen.Brush, rect.Size))
using (var wrapper = CreateBrush(pen.Brush, rect))
using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext))
{
if (wrapper.PlatformBrush != null)
@ -349,7 +347,7 @@ namespace Avalonia.Direct2D1.Media
{
var impl = (FormattedTextImpl)text;
using (var brush = CreateBrush(foreground, impl.Bounds.Size))
using (var brush = CreateBrush(foreground, impl.Bounds))
using (var renderer = new AvaloniaTextRenderer(this, _deviceContext, brush.PlatformBrush))
{
if (brush.PlatformBrush != null)
@ -367,7 +365,7 @@ namespace Avalonia.Direct2D1.Media
/// <param name="glyphRun">The glyph run.</param>
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
{
using (var brush = CreateBrush(foreground, glyphRun.Size))
using (var brush = CreateBrush(foreground, new Rect(glyphRun.Size)))
{
var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
@ -458,9 +456,9 @@ namespace Avalonia.Direct2D1.Media
/// Creates a Direct2D brush wrapper for a Avalonia brush.
/// </summary>
/// <param name="brush">The avalonia brush.</param>
/// <param name="destinationSize">The size of the brush's target area.</param>
/// <param name="destinationRect">The brush's target area.</param>
/// <returns>The Direct2D brush wrapper.</returns>
public BrushImpl CreateBrush(IBrush brush, Size destinationSize)
public BrushImpl CreateBrush(IBrush brush, Rect destinationRect)
{
var solidColorBrush = brush as ISolidColorBrush;
var linearGradientBrush = brush as ILinearGradientBrush;
@ -475,11 +473,11 @@ namespace Avalonia.Direct2D1.Media
}
else if (linearGradientBrush != null)
{
return new LinearGradientBrushImpl(linearGradientBrush, _deviceContext, destinationSize);
return new LinearGradientBrushImpl(linearGradientBrush, _deviceContext, destinationRect);
}
else if (radialGradientBrush != null)
{
return new RadialGradientBrushImpl(radialGradientBrush, _deviceContext, destinationSize);
return new RadialGradientBrushImpl(radialGradientBrush, _deviceContext, destinationRect);
}
else if (conicGradientBrush != null)
{
@ -492,7 +490,7 @@ namespace Avalonia.Direct2D1.Media
imageBrush,
_deviceContext,
(BitmapImpl)imageBrush.Source.PlatformImpl.Item,
destinationSize);
destinationRect.Size);
}
else if (visualBrush != null)
{
@ -523,7 +521,7 @@ namespace Avalonia.Direct2D1.Media
visualBrush,
_deviceContext,
new D2DBitmapImpl(intermediate.Bitmap),
destinationSize);
destinationRect.Size);
}
}
}
@ -574,7 +572,7 @@ namespace Avalonia.Direct2D1.Media
ContentBounds = PrimitiveExtensions.RectangleInfinite,
MaskTransform = PrimitiveExtensions.Matrix3x2Identity,
Opacity = 1,
OpacityBrush = CreateBrush(mask, bounds.Size).PlatformBrush
OpacityBrush = CreateBrush(mask, bounds).PlatformBrush
};
var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_deviceContext);
_deviceContext.PushLayer(ref parameters, layer);

7
src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs

@ -8,7 +8,7 @@ namespace Avalonia.Direct2D1.Media
public LinearGradientBrushImpl(
ILinearGradientBrush brush,
SharpDX.Direct2D1.RenderTarget target,
Size destinationSize)
Rect destinationRect)
{
if (brush.GradientStops.Count == 0)
{
@ -21,8 +21,9 @@ namespace Avalonia.Direct2D1.Media
Position = (float)s.Offset
}).ToArray();
var startPoint = brush.StartPoint.ToPixels(destinationSize);
var endPoint = brush.EndPoint.ToPixels(destinationSize);
var position = destinationRect.Position;
var startPoint = position + brush.StartPoint.ToPixels(destinationRect.Size);
var endPoint = position + brush.EndPoint.ToPixels(destinationRect.Size);
using (var stops = new SharpDX.Direct2D1.GradientStopCollection(
target,

11
src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs

@ -8,7 +8,7 @@ namespace Avalonia.Direct2D1.Media
public RadialGradientBrushImpl(
IRadialGradientBrush brush,
SharpDX.Direct2D1.RenderTarget target,
Size destinationSize)
Rect destinationRect)
{
if (brush.GradientStops.Count == 0)
{
@ -21,12 +21,13 @@ namespace Avalonia.Direct2D1.Media
Position = (float)s.Offset
}).ToArray();
var centerPoint = brush.Center.ToPixels(destinationSize);
var gradientOrigin = brush.GradientOrigin.ToPixels(destinationSize) - centerPoint;
var position = destinationRect.Position;
var centerPoint = position + brush.Center.ToPixels(destinationRect.Size);
var gradientOrigin = position + brush.GradientOrigin.ToPixels(destinationRect.Size) - centerPoint;
// Note: Direct2D supports RadiusX and RadiusY but Cairo backend supports only Radius property
var radiusX = brush.Radius * destinationSize.Width;
var radiusY = brush.Radius * destinationSize.Height;
var radiusX = brush.Radius * destinationRect.Width;
var radiusY = brush.Radius * destinationRect.Height;
using (var stops = new SharpDX.Direct2D1.GradientStopCollection(
target,

2
src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs

@ -117,7 +117,7 @@ namespace Avalonia.Win32
{
var visual = window.Renderer.HitTestFirst(position, _owner as Window, x =>
{
if (x is IInputElement ie && !ie.IsHitTestVisible)
if (x is IInputElement ie && (!ie.IsHitTestVisible || !ie.IsVisible))
{
return false;
}

46
tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs

@ -1,5 +1,9 @@
using Avalonia.Input.Raw;
using System;
using System.Windows.Input;
using Avalonia.Controls;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.UnitTests;
using Moq;
using Xunit;
@ -86,5 +90,45 @@ namespace Avalonia.Input.UnitTests
focused.Verify(x => x.RaiseEvent(It.IsAny<TextInputEventArgs>()));
}
[Fact]
public void Can_Change_KeyBindings_In_Keybinding_Event_Handler()
{
var target = new KeyboardDevice();
var button = new Button();
var root = new TestRoot(button);
var raised = 0;
button.KeyBindings.Add(new KeyBinding
{
Gesture = new KeyGesture(Key.O, KeyModifiers.Control),
Command = new DelegateCommand(() =>
{
button.KeyBindings.Clear();
++raised;
}),
});
target.SetFocusedElement(button, NavigationMethod.Pointer, 0);
target.ProcessRawEvent(
new RawKeyEventArgs(
target,
0,
root,
RawKeyEventType.KeyDown,
Key.O,
RawInputModifiers.Control));
Assert.Equal(1, raised);
}
private class DelegateCommand : ICommand
{
private readonly Action _action;
public DelegateCommand(Action action) => _action = action;
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter) => true;
public void Execute(object parameter) => _action();
}
}
}

40
tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs

@ -1,5 +1,6 @@
using Avalonia.Controls;
using Avalonia.Media;
using System;
using System.Threading.Tasks;
using Xunit;
@ -174,5 +175,44 @@ namespace Avalonia.Direct2D1.RenderTests.Media
await RenderToFile(target);
CompareImages();
}
[Fact]
public async Task ConicGradientBrush_DrawingContext()
{
var brush = new ConicGradientBrush
{
GradientStops =
{
new GradientStop { Color = Colors.Red, Offset = 0 },
new GradientStop { Color = Colors.Yellow, Offset = 0.1667 },
new GradientStop { Color = Colors.Lime, Offset = 0.3333 },
new GradientStop { Color = Colors.Aqua, Offset = 0.5000 },
new GradientStop { Color = Colors.Blue, Offset = 0.6667 },
new GradientStop { Color = Colors.Magenta, Offset = 0.8333 },
new GradientStop { Color = Colors.Red, Offset = 1 },
}
};
Decorator target = new Decorator
{
Width = 200,
Height = 200,
Child = new DrawnControl(c =>
{
c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100));
c.DrawRectangle(brush, null, new Rect(100, 100, 100, 100));
}),
};
await RenderToFile(target);
CompareImages();
}
private class DrawnControl : Control
{
private readonly Action<DrawingContext> _render;
public DrawnControl(Action<DrawingContext> render) => _render = render;
public override void Render(DrawingContext context) => _render(context);
}
}
}

36
tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs

@ -72,5 +72,41 @@ namespace Avalonia.Direct2D1.RenderTests.Media
await RenderToFile(target);
CompareImages();
}
[Fact]
public async Task LinearGradientBrush_DrawingContext()
{
var brush = new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
GradientStops =
{
new GradientStop { Color = Colors.Red, Offset = 0 },
new GradientStop { Color = Colors.Blue, Offset = 1 }
}
};
Decorator target = new Decorator
{
Width = 200,
Height = 200,
Child = new DrawnControl(c =>
{
c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100));
c.DrawRectangle(brush, null, new Rect(100, 100, 100, 100));
}),
};
await RenderToFile(target);
CompareImages();
}
private class DrawnControl : Control
{
private readonly Action<DrawingContext> _render;
public DrawnControl(Action<DrawingContext> render) => _render = render;
public override void Render(DrawingContext context) => _render(context);
}
}
}

34
tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs

@ -165,5 +165,39 @@ namespace Avalonia.Direct2D1.RenderTests.Media
await RenderToFile(target);
CompareImages();
}
[Fact]
public async Task RadialGradientBrush_DrawingContext()
{
var brush = new RadialGradientBrush
{
GradientStops =
{
new GradientStop { Color = Colors.Red, Offset = 0 },
new GradientStop { Color = Colors.Blue, Offset = 1 }
}
};
Decorator target = new Decorator
{
Width = 200,
Height = 200,
Child = new DrawnControl(c =>
{
c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100));
c.DrawRectangle(brush, null, new Rect(100, 100, 100, 100));
}),
};
await RenderToFile(target);
CompareImages();
}
private class DrawnControl : Control
{
private readonly Action<DrawingContext> _render;
public DrawnControl(Action<DrawingContext> render) => _render = render;
public override void Render(DrawingContext context) => _render(context);
}
}
}

23
tests/Avalonia.Styling.UnitTests/SelectorTests_PropertyEquals.cs

@ -22,6 +22,23 @@ namespace Avalonia.Styling.UnitTests
Assert.False(await activator.Take(1));
}
[Theory]
[InlineData("Bar", FooBar.Bar)]
[InlineData("352", 352)]
[InlineData("0.1", 0.1)]
public async Task PropertyEquals_Matches_When_Property_Has_Matching_Value_And_Different_Type(string literal, object value)
{
var control = new TextBlock();
var target = default(Selector).PropertyEquals(TextBlock.TagProperty, literal);
var activator = target.Match(control).Activator.ToObservable();
Assert.False(await activator.Take(1));
control.Tag = value;
Assert.True(await activator.Take(1));
control.Tag = null;
Assert.False(await activator.Take(1));
}
[Fact]
public void OfType_PropertyEquals_Doesnt_Match_Control_Of_Wrong_Type()
{
@ -40,5 +57,11 @@ namespace Avalonia.Styling.UnitTests
Assert.Equal("TextBlock[Text=foo]", target.ToString());
}
private enum FooBar
{
Foo,
Bar
}
}
}

6
tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs

@ -59,9 +59,9 @@ namespace Avalonia.Visuals.UnitTests.Media
}
[Fact]
public void Equality_Is_Implemented_Between_Immutable_And_Mmutable_Pens()
public void Equality_Is_Implemented_Between_Immutable_And_Mutable_Pens()
{
var brush = new SolidColorBrush(Colors.Red);
var brush = new ImmutableSolidColorBrush(Colors.Red);
var target1 = new ImmutablePen(
brush: brush,
thickness: 2,
@ -83,7 +83,7 @@ namespace Avalonia.Visuals.UnitTests.Media
[Fact]
public void Equality_Is_Implemented_Between_Mutable_And_Immutable_DashStyles()
{
var brush = new SolidColorBrush(Colors.Red);
var brush = new ImmutableSolidColorBrush(Colors.Red);
var target1 = new ImmutablePen(
brush: brush,
thickness: 2,

BIN
tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_DrawingContext.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

BIN
tests/TestFiles/Direct2D1/Media/LinearGradientBrush/LinearGradientBrush_DrawingContext.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_DrawingContext.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_DrawingContext.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_DrawingContext.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_DrawingContext.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Loading…
Cancel
Save