Browse Source

Merge branch 'master' into windowing-prototype

thread-issue-repro
danwalmsley 8 years ago
committed by GitHub
parent
commit
788c96817e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .gitignore
  2. 26
      samples/BindingDemo/MainWindow.xaml
  3. 6
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
  4. 6
      samples/ControlCatalog/Pages/BorderPage.xaml
  5. 10
      samples/ControlCatalog/Pages/ButtonPage.xaml
  6. 6
      samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml
  7. 6
      samples/ControlCatalog/Pages/CalendarPage.xaml
  8. 4
      samples/ControlCatalog/Pages/CanvasPage.xaml
  9. 10
      samples/ControlCatalog/Pages/CarouselPage.xaml
  10. 10
      samples/ControlCatalog/Pages/CheckBoxPage.xaml
  11. 6
      samples/ControlCatalog/Pages/ContextMenuPage.xaml
  12. 6
      samples/ControlCatalog/Pages/DatePickerPage.xaml
  13. 4
      samples/ControlCatalog/Pages/DialogsPage.xaml
  14. 6
      samples/ControlCatalog/Pages/DragAndDropPage.xaml
  15. 6
      samples/ControlCatalog/Pages/DropDownPage.xaml
  16. 6
      samples/ControlCatalog/Pages/ExpanderPage.xaml
  17. 4
      samples/ControlCatalog/Pages/ImagePage.xaml
  18. 4
      samples/ControlCatalog/Pages/MenuPage.xaml
  19. 6
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  20. 6
      samples/ControlCatalog/Pages/ProgressBarPage.xaml
  21. 8
      samples/ControlCatalog/Pages/RadioButtonPage.xaml
  22. 4
      samples/ControlCatalog/Pages/SliderPage.xaml
  23. 11
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  24. 2
      samples/ControlCatalog/Pages/ToolTipPage.xaml
  25. 4
      samples/ControlCatalog/Pages/TreeViewPage.xaml
  26. 2
      samples/VirtualizationDemo/MainWindow.xaml
  27. 65
      src/Avalonia.Animation/Animation.cs
  28. 66
      src/Avalonia.Animation/AnimatorKeyFrame.cs
  29. 188
      src/Avalonia.Animation/AnimatorStateMachine`1.cs
  30. 75
      src/Avalonia.Animation/Animator`1.cs
  31. 2
      src/Avalonia.Animation/Cue.cs
  32. 8
      src/Avalonia.Animation/DoubleAnimator.cs
  33. 2
      src/Avalonia.Animation/IAnimationSetter.cs
  34. 16
      src/Avalonia.Animation/KeyFrame.cs
  35. 8
      src/Avalonia.Animation/KeyFramePair`1.cs
  36. 2
      src/Avalonia.Base/AvaloniaObject.cs
  37. 52
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  38. 25
      src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs
  39. 6
      src/Avalonia.Base/Utilities/CharacterReader.cs
  40. 6
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  41. 2
      src/Avalonia.Base/ValueStore.cs
  42. 9
      src/Avalonia.Controls/Image.cs
  43. 6
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  44. 2
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  45. 90
      src/Avalonia.Controls/ProgressBar.cs
  46. 5
      src/Avalonia.Controls/Slider.cs
  47. 38
      src/Avalonia.Controls/StackPanel.cs
  48. 14
      src/Avalonia.Controls/TextBox.cs
  49. 12
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  50. 10
      src/Avalonia.Controls/Window.cs
  51. 4
      src/Avalonia.Controls/WindowCollection.cs
  52. 12
      src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj
  53. 4
      src/Avalonia.Diagnostics/DevTools.xaml
  54. 1
      src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
  55. 4
      src/Avalonia.Diagnostics/Views/TreePageView.xaml
  56. 6
      src/Avalonia.Styling/Styling/Setter.cs
  57. 45
      src/Avalonia.Themes.Default/ProgressBar.xaml
  58. 10
      src/Avalonia.Visuals/Media/DrawingContext.cs
  59. 19
      src/Avalonia.Visuals/Media/ITileBrush.cs
  60. 31
      src/Avalonia.Visuals/Media/Imaging/BitmapInterpolationMode.cs
  61. 11
      src/Avalonia.Visuals/Media/Immutable/ImmutableImageBrush.cs
  62. 16
      src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs
  63. 12
      src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs
  64. 39
      src/Avalonia.Visuals/Media/RenderOptions.cs
  65. 19
      src/Avalonia.Visuals/Media/TileBrush.cs
  66. 4
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  67. 7
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  68. 23
      src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs
  69. 7
      src/Gtk/Avalonia.Gtk3/KeyTransform.cs
  70. 1
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  71. 69
      src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs
  72. 160
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  73. 85
      src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs
  74. 4
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaRuntimeTypeProvider.cs
  75. 98
      src/Markup/Avalonia.Markup/Data/Binding.cs
  76. 11
      src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs
  77. 15
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs
  78. 158
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs
  79. 39
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs
  80. 54
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs
  81. 12
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs
  82. 4
      src/OSX/Avalonia.MonoMac/SystemDialogsImpl.cs
  83. 38
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  84. 25
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  85. 9
      src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs
  86. 11
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs
  87. 22
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  88. 39
      tests/Avalonia.Controls.UnitTests/SliderTests.cs
  89. 14
      tests/Avalonia.Controls.UnitTests/StackPanelTests.cs
  90. 3
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs
  91. 36
      tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs
  92. 226
      tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs
  93. 29
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
  94. 55
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs
  95. 54
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  96. 6
      tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs
  97. 14
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs

5
.gitignore

@ -165,6 +165,11 @@ $RECYCLE.BIN/
#################
.idea
#################
## VS Code
#################
.vscode/
#################
## Cake
#################

26
samples/BindingDemo/MainWindow.xaml

@ -18,18 +18,18 @@
<TabItem Header="Basic">
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<StackPanel Margin="18" Gap="4" Width="200">
<StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Simple Bindings"/>
<TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue}" Name="first"/>
<TextBox Watermark="One Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneWay}"/>
<TextBox Watermark="One Time" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneTime}"/>
</StackPanel>
<StackPanel Margin="18" Gap="4" Width="200">
<StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Collection Bindings"/>
<TextBox Watermark="Items[1].StringValue" UseFloatingWatermark="True" Text="{Binding Path=Items[1].StringValue}"/>
<Button Command="{Binding ShuffleItems}">Shuffle</Button>
</StackPanel>
<StackPanel Margin="18" Gap="4" Width="200">
<StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Negated Bindings"/>
<TextBox Watermark="Boolean String" UseFloatingWatermark="True" Text="{Binding Path=BooleanString}"/>
<CheckBox IsChecked="{Binding !BooleanString}">!BooleanString</CheckBox>
@ -37,13 +37,13 @@
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal">
<StackPanel Margin="18" Gap="4" Width="200" HorizontalAlignment="Left">
<StackPanel Margin="18" Spacing="4" Width="200" HorizontalAlignment="Left">
<TextBlock FontSize="16" Text="Numeric Bindings"/>
<TextBox Watermark="Double" UseFloatingWatermark="True" Text="{Binding Path=DoubleValue, Mode=TwoWay}"/>
<TextBlock Text="{Binding Path=DoubleValue}"/>
<ProgressBar Maximum="10" Value="{Binding DoubleValue}"/>
</StackPanel>
<StackPanel Margin="18" Gap="4" Width="200" HorizontalAlignment="Left">
<StackPanel Margin="18" Spacing="4" Width="200" HorizontalAlignment="Left">
<TextBlock FontSize="16" Text="Binding Sources"/>
<TextBox Watermark="Value of first TextBox" UseFloatingWatermark="True"
Text="{Binding #first.Text, Mode=TwoWay}"/>
@ -52,7 +52,7 @@
<TextBox Watermark="Value of SharedItem.StringValue (duplicate)" UseFloatingWatermark="True"
Text="{Binding StringValue, Source={StaticResource SharedItem}, Mode=TwoWay}"/>
</StackPanel>
<StackPanel Margin="18" Gap="4" Width="200" HorizontalAlignment="Left">
<StackPanel Margin="18" Spacing="4" Width="200" HorizontalAlignment="Left">
<TextBlock FontSize="16" Text="Scheduler"/>
<TextBox Watermark="Background Thread" Text="{Binding CurrentTime, Mode=OneWay}"/>
<TextBlock FontSize="16" Text="Stream Operator"/>
@ -68,11 +68,11 @@
<TextBlock Text="{Binding StringValue}"/>
</DataTemplate>
</StackPanel.DataTemplates>
<StackPanel Margin="18" Gap="4" Width="200">
<StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Multiple"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" SelectedItems="{Binding SelectedItems}"/>
</StackPanel>
<StackPanel Margin="18" Gap="4" Width="200">
<StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Multiple"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" SelectedItems="{Binding SelectedItems}"/>
</StackPanel>
@ -87,16 +87,16 @@
</TabItem>
<TabItem Header="Property Validation">
<StackPanel Orientation="Horizontal">
<StackPanel Margin="18" Gap="4" MinWidth="200" DataContext="{Binding ExceptionDataValidation}">
<StackPanel Margin="18" Spacing="4" MinWidth="200" DataContext="{Binding ExceptionDataValidation}">
<TextBlock FontSize="16" Text="Exception Validation"/>
<TextBox Watermark="Less Than 10" UseFloatingWatermark="True" Text="{Binding Path=LessThan10}"/>
</StackPanel>
<StackPanel Margin="18" Gap="4" MinWidth="200" DataContext="{Binding IndeiDataValidation}">
<StackPanel Margin="18" Spacing="4" MinWidth="200" DataContext="{Binding IndeiDataValidation}">
<TextBlock FontSize="16" Text="INotifyDataErrorInfo Validation"/>
<TextBox Watermark="Maximum" UseFloatingWatermark="True" Text="{Binding Path=Maximum}"/>
<TextBox Watermark="Value" UseFloatingWatermark="True" Text="{Binding Path=Value}"/>
</StackPanel>
<StackPanel Margin="18" Gap="4" MinWidth="200" DataContext="{Binding DataAnnotationsValidation}">
<StackPanel Margin="18" Spacing="4" MinWidth="200" DataContext="{Binding DataAnnotationsValidation}">
<TextBlock FontSize="16" Text="Data Annotations Validation"/>
<TextBox Watermark="Phone #" UseFloatingWatermark="True" Text="{Binding PhoneNumber}"/>
<TextBox Watermark="Less Than 10" UseFloatingWatermark="True" Text="{Binding Path=LessThan10}"/>
@ -104,7 +104,7 @@
</StackPanel>
</TabItem>
<TabItem Header="Commands">
<StackPanel Margin="18" Gap="4" Width="200">
<StackPanel Margin="18" Spacing="4" Width="200">
<Button Content="Button" Command="{Binding StringValueCommand}" CommandParameter="Button"/>
<ToggleButton Content="ToggleButton" IsChecked="{Binding BooleanFlag, Mode=OneWay}" Command="{Binding StringValueCommand}" CommandParameter="ToggleButton"/>
<CheckBox Content="CheckBox" IsChecked="{Binding !BooleanFlag, Mode=OneWay}" Command="{Binding StringValueCommand}" CommandParameter="CheckBox"/>
@ -114,4 +114,4 @@
</StackPanel>
</TabItem>
</TabControl>
</Window>
</Window>

6
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml

@ -1,12 +1,12 @@
<UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">AutoCompleteBox</TextBlock>
<TextBlock Classes="h2">A control into which the user can input text</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Gap="8">
Spacing="8">
<StackPanel Orientation="Vertical">
<TextBlock Text="MinimumPrefixLength: 1"/>
<AutoCompleteBox Width="200"
@ -56,4 +56,4 @@
</StackPanel>
</StackPanel>
</StackPanel>
</UserControl>
</UserControl>

6
samples/ControlCatalog/Pages/BorderPage.xaml

@ -1,12 +1,12 @@
<UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Border</TextBlock>
<TextBlock Classes="h2">A control which decorates a child with a border and background</TextBlock>
<StackPanel Orientation="Vertical"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Gap="16">
Spacing="16">
<Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16">
<TextBlock>Border</TextBlock>
</Border>
@ -29,4 +29,4 @@
</Border>
</StackPanel>
</StackPanel>
</UserControl>
</UserControl>

10
samples/ControlCatalog/Pages/ButtonPage.xaml

@ -1,14 +1,14 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Button</TextBlock>
<TextBlock Classes="h2">A button control</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Gap="16">
<StackPanel Orientation="Vertical" Gap="8" Width="150">
Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8" Width="150">
<Button>Button</Button>
<Button Foreground="White">Foreground</Button>
<Button Background="{DynamicResource ThemeAccentBrush}">Background</Button>
@ -25,7 +25,7 @@
</Button>
</StackPanel>
<StackPanel Orientation="Vertical" Gap="8" Width="150">
<StackPanel Orientation="Vertical" Spacing="8" Width="150">
<Button BorderThickness="0">No Border</Button>
<Button BorderBrush="{DynamicResource ThemeAccentBrush}">Border Color</Button>
<Button BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="4">Thick Border</Button>
@ -33,4 +33,4 @@
</StackPanel>
</StackPanel>
</StackPanel>
</UserControl>
</UserControl>

6
samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml

@ -1,11 +1,11 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">ButtonSpinner</TextBlock>
<TextBlock Classes="h2">The ButtonSpinner control allows you to add button spinners to any element and then respond to the Spin event to manipulate that element.</TextBlock>
<StackPanel Orientation="Vertical" Gap="8" Width="200" Margin="0,20,0,0">
<StackPanel Orientation="Vertical" Spacing="8" Width="200" Margin="0,20,0,0">
<CheckBox Name="allowSpinCheck" IsChecked="True">AllowSpin</CheckBox>
<CheckBox Name="showSpinCheck" IsChecked="True">ShowButtonSpinner</CheckBox>
<ButtonSpinner Spin="OnSpin" Height="30"
@ -21,4 +21,4 @@
</StackPanel>
</StackPanel>
</UserControl>
</UserControl>

6
samples/ControlCatalog/Pages/CalendarPage.xaml

@ -1,13 +1,13 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Calendar</TextBlock>
<TextBlock Classes="h2">A calendar control for selecting dates</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Gap="16">
Spacing="16">
<StackPanel Orientation="Vertical">
<TextBlock Text="SelectionMode: None"/>
<Calendar SelectionMode="None"
@ -44,4 +44,4 @@
</StackPanel>
</StackPanel>
</UserControl>
</UserControl>

4
samples/ControlCatalog/Pages/CanvasPage.xaml

@ -1,5 +1,5 @@
<UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Canvas</TextBlock>
<TextBlock Classes="h2">A panel which lays out its children by explicit coordinates</TextBlock>
<Canvas Background="Yellow" Width="300" Height="400">
@ -31,4 +31,4 @@
<Polyline Points="0,0 65,0 78,-26 91,39 104,-39 117,13 130,0 195,0" Stroke="Brown" Canvas.Left="30" Canvas.Top="350"/>
</Canvas>
</StackPanel>
</UserControl>
</UserControl>

10
samples/ControlCatalog/Pages/CarouselPage.xaml

@ -1,9 +1,9 @@
<UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Carousel</TextBlock>
<TextBlock Classes="h2">An items control that displays its items as pages that fill the control.</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Gap="8">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Spacing="8">
<Button Name="left" VerticalAlignment="Center" Padding="20">
<Path Data="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z" Fill="Black"/>
</Button>
@ -20,7 +20,7 @@
</Button>
</StackPanel>
<StackPanel Orientation="Horizontal" Gap="4">
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock VerticalAlignment="Center">Transition</TextBlock>
<DropDown Name="transition" SelectedIndex="1" VerticalAlignment="Center">
<DropDownItem>None</DropDownItem>
@ -29,7 +29,7 @@
</DropDown>
</StackPanel>
<StackPanel Orientation="Horizontal" Gap="4">
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock VerticalAlignment="Center">Orientation</TextBlock>
<DropDown Name="orientation" SelectedIndex="1" VerticalAlignment="Center">
<DropDownItem>Horizontal</DropDownItem>
@ -38,4 +38,4 @@
</StackPanel>
</StackPanel>
</UserControl>
</UserControl>

10
samples/ControlCatalog/Pages/CheckBoxPage.xaml

@ -1,15 +1,15 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">CheckBox</TextBlock>
<TextBlock Classes="h2">A check box control</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Gap="16">
Spacing="16">
<StackPanel Orientation="Vertical"
Gap="16">
Spacing="16">
<CheckBox>Unchecked</CheckBox>
<CheckBox IsChecked="True">Checked</CheckBox>
<CheckBox IsChecked="{x:Null}">Indeterminate</CheckBox>
@ -17,7 +17,7 @@
</StackPanel>
<StackPanel Orientation="Vertical"
HorizontalAlignment="Center"
Gap="16">
Spacing="16">
<CheckBox IsChecked="False" IsThreeState="True">Three State: Unchecked</CheckBox>
<CheckBox IsChecked="True" IsThreeState="True">Three State: Checked</CheckBox>
<CheckBox IsChecked="{x:Null}" IsThreeState="True">Three State: Indeterminate</CheckBox>
@ -25,4 +25,4 @@
</StackPanel>
</StackPanel>
</StackPanel>
</UserControl>
</UserControl>

6
samples/ControlCatalog/Pages/ContextMenuPage.xaml

@ -1,12 +1,12 @@
<UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Context Menu</TextBlock>
<TextBlock Classes="h2">A right click menu that can be applied to any control.</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Gap="16">
Spacing="16">
<Border Background="{DynamicResource ThemeAccentBrush}"
Padding="48,48,48,48">
<Border.ContextMenu>
@ -33,4 +33,4 @@
</Border>
</StackPanel>
</StackPanel>
</UserControl>
</UserControl>

6
samples/ControlCatalog/Pages/DatePickerPage.xaml

@ -1,13 +1,13 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">DatePicker</TextBlock>
<TextBlock Classes="h2">A control for selecting dates with a calendar drop-down</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Gap="16">
Spacing="16">
<StackPanel Orientation="Vertical"
Width="200">
<TextBlock Text="SelectedDateFormat: Short"/>
@ -43,4 +43,4 @@
</StackPanel>
</StackPanel>
</UserControl>
</UserControl>

4
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -1,5 +1,5 @@
<UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4" Margin="4">
<StackPanel Orientation="Vertical" Spacing="4" Margin="4">
<Button Name="OpenFile">Open File</Button>
<Button Name="SaveFile">Save File</Button>
<Button Name="SelectFolder">Select Folder</Button>
@ -9,4 +9,4 @@
</StackPanel>
<Button Name="DecoratedWindow">Decorated window</Button>
</StackPanel>
</UserControl>
</UserControl>

6
samples/ControlCatalog/Pages/DragAndDropPage.xaml

@ -1,12 +1,12 @@
<UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Drag+Drop</TextBlock>
<TextBlock Classes="h2">Example of Drag+Drop capabilities</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Gap="16">
Spacing="16">
<Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16" Name="DragMe">
<TextBlock Name="DragState">Drag Me</TextBlock>
</Border>
@ -16,4 +16,4 @@
</Border>
</StackPanel>
</StackPanel>
</UserControl>
</UserControl>

6
samples/ControlCatalog/Pages/DropDownPage.xaml

@ -1,9 +1,9 @@
<UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">DropDown</TextBlock>
<TextBlock Classes="h2">A drop-down list.</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Gap="8">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Spacing="8">
<DropDown SelectedIndex="0">
<DropDownItem>Inline Items</DropDownItem>
<DropDownItem>Inline Item 2</DropDownItem>
@ -28,4 +28,4 @@
</StackPanel>
</StackPanel>
</UserControl>
</UserControl>

6
samples/ControlCatalog/Pages/ExpanderPage.xaml

@ -1,12 +1,12 @@
<UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Expander</TextBlock>
<TextBlock Classes="h2">Expands to show nested content</TextBlock>
<StackPanel Orientation="Vertical"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Gap="16">
Spacing="16">
<Expander Header="Expand Up" ExpandDirection="Up">
<StackPanel>
<TextBlock>Expanded content</TextBlock>
@ -29,4 +29,4 @@
</Expander>
</StackPanel>
</StackPanel>
</UserControl>
</UserControl>

4
samples/ControlCatalog/Pages/ImagePage.xaml

@ -1,12 +1,12 @@
<UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Image</TextBlock>
<TextBlock Classes="h2">Displays an image</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Gap="16">
Spacing="16">
<StackPanel Orientation="Vertical">
<TextBlock>No Stretch</TextBlock>
<Image Source="resm:ControlCatalog.Assets.delicate-arch-896885_640.jpg"

4
samples/ControlCatalog/Pages/MenuPage.xaml

@ -1,12 +1,12 @@
<UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Menu</TextBlock>
<TextBlock Classes="h2">A window menu</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Gap="16">
Spacing="16">
<Menu>
<MenuItem Header="_First">
<MenuItem Header="Standard _Menu Item"/>

6
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@ -1,6 +1,6 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Margin="2" Classes="h1">Numeric up-down control</TextBlock>
<TextBlock Margin="2" Classes="h2" TextWrapping="Wrap">Numeric up-down control provides a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel.</TextBlock>
@ -26,7 +26,7 @@
VerticalAlignment="Center" Margin="2">
<DropDown.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Gap="2">
<StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="-"/>
<TextBlock Text="{Binding Value}"/>
@ -69,7 +69,7 @@
</Grid>
</Grid>
<StackPanel Margin="2,10,2,2" Orientation="Horizontal" Gap="10">
<StackPanel Margin="2,10,2,2" Orientation="Horizontal" Spacing="10">
<TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of NumericUpDown:</TextBlock>
<NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5"
CultureInfo="en-US" VerticalAlignment="Center" Height="25" Width="100"

6
samples/ControlCatalog/Pages/ProgressBarPage.xaml

@ -1,5 +1,5 @@
<UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">ProgressBar</TextBlock>
<TextBlock Classes="h2">A progress bar control</TextBlock>
@ -7,8 +7,8 @@
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Gap="16">
<StackPanel Gap="16">
Spacing="16">
<StackPanel Spacing="16">
<ProgressBar Value="{Binding #hprogress.Value}" />
<ProgressBar IsIndeterminate="True"/>
</StackPanel>

8
samples/ControlCatalog/Pages/RadioButtonPage.xaml

@ -1,22 +1,22 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">RadioButton</TextBlock>
<TextBlock Classes="h2">Allows the selection of a single option of many</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Gap="16">
Spacing="16">
<StackPanel Orientation="Vertical"
Gap="16">
Spacing="16">
<RadioButton IsChecked="True">Option 1</RadioButton>
<RadioButton>Option 2</RadioButton>
<RadioButton IsChecked="{x:Null}">Option 3</RadioButton>
<RadioButton IsEnabled="False">Disabled</RadioButton>
</StackPanel>
<StackPanel Orientation="Vertical"
Gap="16">
Spacing="16">
<RadioButton IsChecked="True" IsThreeState="True">Three States: Option 1</RadioButton>
<RadioButton IsChecked="False" IsThreeState="True">Three States: Option 2</RadioButton>
<RadioButton IsChecked="{x:Null}" IsThreeState="True">Three States: Option 3</RadioButton>

4
samples/ControlCatalog/Pages/SliderPage.xaml

@ -1,9 +1,9 @@
<UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Slider</TextBlock>
<TextBlock Classes="h2">A control that lets the user select from a range of values by moving a Thumb control along a Track.</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Gap="16">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Spacing="16">
<Slider Value="0"
Minimum="0"
Maximum="100"

11
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -1,14 +1,15 @@
<UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">TextBox</TextBlock>
<TextBlock Classes="h2">A control into which the user can input text</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Gap="16">
<StackPanel Orientation="Vertical" Gap="8">
Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8">
<TextBox Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit." Width="200" />
<TextBox Watermark="ReadOnly" IsReadOnly="True" Text="This is read only"/>
<TextBox Width="200" Watermark="Watermark" />
<TextBox Width="200"
Watermark="Floating Watermark"
@ -25,13 +26,13 @@
<TextBox Width="200" Text="Right aligned text" TextAlignment="Right" />
</StackPanel>
<StackPanel Orientation="Vertical" Gap="8">
<StackPanel Orientation="Vertical" Spacing="8">
<TextBox AcceptsReturn="True" TextWrapping="Wrap" Width="200" Height="125"
Text="Multiline TextBox with TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
<TextBox AcceptsReturn="True" Width="200" Height="125"
Text="Multiline TextBox with no TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
</StackPanel>
<StackPanel Orientation="Vertical" Gap="8">
<StackPanel Orientation="Vertical" Spacing="8">
<TextBox Width="200" Text="Custom font regular" FontWeight="Normal" FontStyle="Normal" FontFamily="resm:ControlCatalog.Assets.Fonts?assembly=ControlCatalog#Source Sans Pro"/>
<TextBox Width="200" Text="Custom font bold" FontWeight="Bold" FontStyle="Normal" FontFamily="resm:ControlCatalog.Assets.Fonts?assembly=ControlCatalog#Source Sans Pro"/>
<TextBox Width="200" Text="Custom font italic" FontWeight="Normal" FontStyle="Italic" FontFamily="resm:ControlCatalog.Assets.Fonts.SourceSansPro-Italic.ttf?assembly=ControlCatalog#Source Sans Pro"/>

2
samples/ControlCatalog/Pages/ToolTipPage.xaml

@ -1,6 +1,6 @@
<UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical"
Gap="4">
Spacing="4">
<TextBlock Classes="h1">ToolTip</TextBlock>
<TextBlock Classes="h2">A control which pops up a hint when a control is hovered</TextBlock>

4
samples/ControlCatalog/Pages/TreeViewPage.xaml

@ -1,12 +1,12 @@
<UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">TreeView</TextBlock>
<TextBlock Classes="h2">Displays a hierachical tree of data.</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Gap="16">
Spacing="16">
<TreeView Items="{Binding}" Width="250" Height="350">
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}">

2
samples/VirtualizationDemo/MainWindow.xaml

@ -6,7 +6,7 @@
<StackPanel DockPanel.Dock="Right"
Margin="16 0 0 0"
MinWidth="150"
Gap="4">
Spacing="4">
<DropDown Items="{Binding VirtualizationModes}"
SelectedItem="{Binding VirtualizationMode}"/>
<DropDown Items="{Binding Orientations}"

65
src/Avalonia.Animation/Animation.cs

@ -68,7 +68,7 @@ namespace Avalonia.Animation
/// <summary>
/// The value fill mode for this animation.
/// </summary>
public FillMode FillMode { get; set; }
public FillMode FillMode { get; set; }
/// <summary>
/// Easing function to be used.
@ -80,10 +80,10 @@ namespace Avalonia.Animation
this.CollectionChanged += delegate { _isChildrenChanged = true; };
}
private void InterpretKeyframes()
private IList<IAnimator> InterpretKeyframes(Animatable control)
{
var handlerList = new List<(Type, AvaloniaProperty)>();
var kfList = new List<AnimatorKeyFrame>();
var handlerList = new List<(Type type, AvaloniaProperty property)>();
var animatorKeyFrames = new List<AnimatorKeyFrame>();
foreach (var keyframe in this)
{
@ -99,41 +99,38 @@ namespace Avalonia.Animation
if (!handlerList.Contains((handler, setter.Property)))
handlerList.Add((handler, setter.Property));
var newKF = new AnimatorKeyFrame()
var cue = keyframe.Cue;
if (keyframe.TimingMode == KeyFrameTimingMode.TimeSpan)
{
Handler = handler,
Property = setter.Property,
Cue = keyframe.Cue,
KeyTime = keyframe.KeyTime,
timeSpanSet = keyframe.timeSpanSet,
cueSet = keyframe.cueSet,
Value = setter.Value
};
kfList.Add(newKF);
cue = new Cue(keyframe.KeyTime.Ticks / Duration.Ticks);
}
var newKF = new AnimatorKeyFrame(handler, cue);
_subscription.Add(newKF.BindSetter(setter, control));
animatorKeyFrames.Add(newKF);
}
}
var newAnimatorInstances = new List<(Type handler, AvaloniaProperty prop, IAnimator inst)>();
var newAnimatorInstances = new List<IAnimator>();
foreach (var handler in handlerList)
foreach (var (handlerType, property) in handlerList)
{
var newInstance = (IAnimator)Activator.CreateInstance(handler.Item1);
newInstance.Property = handler.Item2;
newAnimatorInstances.Add((handler.Item1, handler.Item2, newInstance));
var newInstance = (IAnimator)Activator.CreateInstance(handlerType);
newInstance.Property = property;
newAnimatorInstances.Add(newInstance);
}
foreach (var kf in kfList)
foreach (var keyframe in animatorKeyFrames)
{
var parent = newAnimatorInstances.Where(p => p.handler == kf.Handler &&
p.prop == kf.Property)
.First();
parent.inst.Add(kf);
var animator = newAnimatorInstances.First(a => a.GetType() == keyframe.AnimatorType &&
a.Property == keyframe.Property);
animator.Add(keyframe);
}
foreach(var instance in newAnimatorInstances)
_animators.Add(instance.inst);
return newAnimatorInstances;
}
/// <summary>
@ -150,17 +147,11 @@ namespace Avalonia.Animation
/// <inheritdocs/>
public IDisposable Apply(Animatable control, IObservable<bool> matchObs)
{
if (_isChildrenChanged)
{
InterpretKeyframes();
_isChildrenChanged = false;
}
foreach (IAnimator keyframes in _animators)
foreach (IAnimator animator in InterpretKeyframes(control))
{
_subscription.Add(keyframes.Apply(this, control, matchObs));
_subscription.Add(animator.Apply(this, control, matchObs));
}
return this;
}
}
}
}

66
src/Avalonia.Animation/AnimatorKeyFrame.cs

@ -4,6 +4,8 @@ using System.Text;
using System.ComponentModel;
using Avalonia.Metadata;
using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Reactive;
namespace Avalonia.Animation
{
@ -11,13 +13,63 @@ namespace Avalonia.Animation
/// Defines a KeyFrame that is used for
/// <see cref="Animator{T}"/> objects.
/// </summary>
public class AnimatorKeyFrame
public class AnimatorKeyFrame : AvaloniaObject
{
public Type Handler;
public Cue Cue;
public TimeSpan KeyTime;
internal bool timeSpanSet, cueSet;
public AvaloniaProperty Property;
public object Value;
public static readonly DirectProperty<AnimatorKeyFrame, object> ValueProperty =
AvaloniaProperty.RegisterDirect<AnimatorKeyFrame, object>(nameof(Value), k => k._value, (k, v) => k._value = v);
public AnimatorKeyFrame()
{
}
public AnimatorKeyFrame(Type animatorType, Cue cue)
{
AnimatorType = animatorType;
Cue = cue;
}
public Type AnimatorType { get; }
public Cue Cue { get; }
public AvaloniaProperty Property { get; private set; }
private object _value;
public object Value
{
get => _value;
set => SetAndRaise(ValueProperty, ref _value, value);
}
public IDisposable BindSetter(IAnimationSetter setter, Animatable targetControl)
{
Property = setter.Property;
var value = setter.Value;
if (value is IBinding binding)
{
return this.Bind(ValueProperty, binding, targetControl);
}
else
{
return this.Bind(ValueProperty, ObservableEx.SingleValue(value).ToBinding(), targetControl);
}
}
public T GetTypedValue<T>()
{
var typeConv = TypeDescriptor.GetConverter(typeof(T));
if (Value == null)
{
throw new ArgumentNullException($"KeyFrame value can't be null.");
}
if (!typeConv.CanConvertTo(Value.GetType()))
{
throw new InvalidCastException($"KeyFrame value doesnt match property type.");
}
return (T)typeConv.ConvertTo(Value, typeof(T));
}
}
}

188
src/Avalonia.Animation/AnimatorStateMachine`1.cs

@ -51,9 +51,9 @@ namespace Avalonia.Animation
Disposed
}
public void Initialize(Animation animation, Animatable control, Animator<T> keyframes)
public void Initialize(Animation animation, Animatable control, Animator<T> animator)
{
_parent = keyframes;
_parent = animator;
_targetAnimation = animation;
_targetControl = control;
_neutralValue = (T)_targetControl.GetValue(_parent.Property);
@ -123,121 +123,133 @@ namespace Avalonia.Animation
double _tempDuration = 0d, _easedTime;
checkstate:
switch (_currentState)
bool handled = false;
while (!handled)
{
case KeyFramesStates.DoDelay:
switch (_currentState)
{
case KeyFramesStates.DoDelay:
if (_fillMode == FillMode.Backward
|| _fillMode == FillMode.Both)
{
if (_currentIteration == 0)
if (_fillMode == FillMode.Backward
|| _fillMode == FillMode.Both)
{
_targetObserver.OnNext(_firstKFValue);
if (_currentIteration == 0)
{
_targetObserver.OnNext(_firstKFValue);
}
else
{
_targetObserver.OnNext(_lastInterpValue);
}
}
if (_delayFrameCount > _delayTotalFrameCount)
{
_currentState = KeyFramesStates.DoRun;
}
else
{
_targetObserver.OnNext(_lastInterpValue);
handled = true;
_delayFrameCount++;
}
}
if (_delayFrameCount > _delayTotalFrameCount)
{
_currentState = KeyFramesStates.DoRun;
goto checkstate;
}
_delayFrameCount++;
break;
case KeyFramesStates.DoRun:
if (_isReversed)
_currentState = KeyFramesStates.RunBackwards;
else
_currentState = KeyFramesStates.RunForwards;
goto checkstate;
case KeyFramesStates.RunForwards:
if (_durationFrameCount > _durationTotalFrameCount)
{
_currentState = KeyFramesStates.RunComplete;
goto checkstate;
}
break;
_tempDuration = (double)_durationFrameCount / _durationTotalFrameCount;
_currentState = KeyFramesStates.RunApplyValue;
case KeyFramesStates.DoRun:
goto checkstate;
if (_isReversed)
_currentState = KeyFramesStates.RunBackwards;
else
_currentState = KeyFramesStates.RunForwards;
case KeyFramesStates.RunBackwards:
break;
if (_durationFrameCount > _durationTotalFrameCount)
{
_currentState = KeyFramesStates.RunComplete;
goto checkstate;
}
case KeyFramesStates.RunForwards:
_tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount;
_currentState = KeyFramesStates.RunApplyValue;
if (_durationFrameCount > _durationTotalFrameCount)
{
_currentState = KeyFramesStates.RunComplete;
}
else
{
_tempDuration = (double)_durationFrameCount / _durationTotalFrameCount;
_currentState = KeyFramesStates.RunApplyValue;
goto checkstate;
}
break;
case KeyFramesStates.RunApplyValue:
case KeyFramesStates.RunBackwards:
_easedTime = _targetAnimation.Easing.Ease(_tempDuration);
if (_durationFrameCount > _durationTotalFrameCount)
{
_currentState = KeyFramesStates.RunComplete;
}
else
{
_tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount;
_currentState = KeyFramesStates.RunApplyValue;
}
break;
_durationFrameCount++;
_lastInterpValue = Interpolator(_easedTime, _neutralValue);
_targetObserver.OnNext(_lastInterpValue);
_currentState = KeyFramesStates.DoRun;
case KeyFramesStates.RunApplyValue:
break;
_easedTime = _targetAnimation.Easing.Ease(_tempDuration);
case KeyFramesStates.RunComplete:
_durationFrameCount++;
_lastInterpValue = Interpolator(_easedTime, _neutralValue);
_targetObserver.OnNext(_lastInterpValue);
_currentState = KeyFramesStates.DoRun;
handled = true;
break;
if (_checkLoopAndRepeat)
{
_delayFrameCount = 0;
_durationFrameCount = 0;
case KeyFramesStates.RunComplete:
if (_isLooping)
{
_currentState = KeyFramesStates.DoRun;
}
else if (_isRepeating)
if (_checkLoopAndRepeat)
{
if (_currentIteration >= _repeatCount)
_delayFrameCount = 0;
_durationFrameCount = 0;
if (_isLooping)
{
_currentState = KeyFramesStates.Stop;
_currentState = KeyFramesStates.DoRun;
}
else
else if (_isRepeating)
{
_currentState = KeyFramesStates.DoRun;
if (_currentIteration >= _repeatCount)
{
_currentState = KeyFramesStates.Stop;
}
else
{
_currentState = KeyFramesStates.DoRun;
}
_currentIteration++;
}
_currentIteration++;
}
if (_animationDirection == PlaybackDirection.Alternate
|| _animationDirection == PlaybackDirection.AlternateReverse)
_isReversed = !_isReversed;
if (_animationDirection == PlaybackDirection.Alternate
|| _animationDirection == PlaybackDirection.AlternateReverse)
_isReversed = !_isReversed;
break;
}
break;
}
_currentState = KeyFramesStates.Stop;
goto checkstate;
_currentState = KeyFramesStates.Stop;
break;
case KeyFramesStates.Stop:
case KeyFramesStates.Stop:
if (_fillMode == FillMode.Forward
|| _fillMode == FillMode.Both)
{
_targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue);
}
_targetObserver.OnCompleted();
break;
if (_fillMode == FillMode.Forward
|| _fillMode == FillMode.Both)
{
_targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue);
}
_targetObserver.OnCompleted();
handled = true;
break;
default:
handled = true;
break;
}
}
}
@ -253,4 +265,4 @@ namespace Avalonia.Animation
_currentState = KeyFramesStates.Disposed;
}
}
}
}

75
src/Avalonia.Animation/Animator`1.cs

@ -19,7 +19,7 @@ namespace Avalonia.Animation
/// <summary>
/// List of type-converted keyframes.
/// </summary>
private Dictionary<double, (T, bool isNeutral)> _convertedKeyframes = new Dictionary<double, (T, bool)>();
private readonly SortedList<double, (AnimatorKeyFrame, bool isNeutral)> _convertedKeyframes = new SortedList<double, (AnimatorKeyFrame, bool)>();
private bool _isVerfifiedAndConverted;
@ -38,12 +38,11 @@ namespace Avalonia.Animation
public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch)
{
if (!_isVerfifiedAndConverted)
VerifyConvertKeyFrames(animation, typeof(T));
VerifyConvertKeyFrames();
return obsMatch
.Where(p => p == true)
// Ignore triggers when global timers are paused.
.Where(p => Timing.GetGlobalPlayState() != PlayState.Pause)
.Where(p => p && Timing.GetGlobalPlayState() != PlayState.Pause)
.Subscribe(_ =>
{
var timerObs = RunKeyFrames(animation, control);
@ -60,8 +59,8 @@ namespace Avalonia.Animation
/// <param name="t">The time parameter, relative to the total animation time</param>
protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t)
{
KeyValuePair<double, (T, bool)> firstCue, lastCue;
int kvCount = _convertedKeyframes.Count();
KeyValuePair<double, (AnimatorKeyFrame frame, bool isNeutral)> firstCue, lastCue;
int kvCount = _convertedKeyframes.Count;
if (kvCount > 2)
{
if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0)
@ -76,8 +75,8 @@ namespace Avalonia.Animation
}
else
{
firstCue = _convertedKeyframes.Where(j => j.Key <= t).Last();
lastCue = _convertedKeyframes.Where(j => j.Key >= t).First();
firstCue = _convertedKeyframes.Last(j => j.Key <= t);
lastCue = _convertedKeyframes.First(j => j.Key >= t);
}
}
else
@ -89,7 +88,9 @@ namespace Avalonia.Animation
double t0 = firstCue.Key;
double t1 = lastCue.Key;
var intraframeTime = (t - t0) / (t1 - t0);
return (intraframeTime, new KeyFramePair<T>(firstCue, lastCue));
var firstFrameData = (firstCue.Value.frame.GetTypedValue<T>(), firstCue.Value.isNeutral);
var lastFrameData = (lastCue.Value.frame.GetTypedValue<T>(), lastCue.Value.isNeutral);
return (intraframeTime, new KeyFramePair<T>(firstFrameData, lastFrameData));
}
@ -98,17 +99,14 @@ namespace Avalonia.Animation
/// </summary>
private IDisposable RunKeyFrames(Animation animation, Animatable control)
{
var _kfStateMach = new AnimatorStateMachine<T>();
_kfStateMach.Initialize(animation, control, this);
var stateMachine = new AnimatorStateMachine<T>();
stateMachine.Initialize(animation, control, this);
Timing.AnimationStateTimer
.TakeWhile(_ => !_kfStateMach._unsubscribe)
.Subscribe(p =>
{
_kfStateMach.Step(p, DoInterpolation);
});
.TakeWhile(_ => !stateMachine._unsubscribe)
.Subscribe(p => stateMachine.Step(p, DoInterpolation));
return control.Bind(Property, _kfStateMach, BindingPriority.Animation);
return control.Bind(Property, stateMachine, BindingPriority.Animation);
}
/// <summary>
@ -119,39 +117,19 @@ namespace Avalonia.Animation
/// <summary>
/// Verifies and converts keyframe values according to this class's target type.
/// </summary>
private void VerifyConvertKeyFrames(Animation animation, Type type)
private void VerifyConvertKeyFrames()
{
var typeConv = TypeDescriptor.GetConverter(type);
foreach (AnimatorKeyFrame k in this)
foreach (AnimatorKeyFrame keyframe in this)
{
if (k.Value == null)
{
throw new ArgumentNullException($"KeyFrame value can't be null.");
}
if (!typeConv.CanConvertTo(k.Value.GetType()))
{
throw new InvalidCastException($"KeyFrame value doesnt match property type.");
}
T convertedValue = (T)typeConv.ConvertTo(k.Value, type);
Cue _normalizedCue = k.Cue;
if (k.timeSpanSet)
{
_normalizedCue = new Cue(k.KeyTime.Ticks / animation.Duration.Ticks);
}
_convertedKeyframes.Add(_normalizedCue.CueValue, (convertedValue, false));
_convertedKeyframes.Add(keyframe.Cue.CueValue, (keyframe, false));
}
SortKeyFrameCues(_convertedKeyframes);
AddNeutralKeyFramesIfNeeded();
_isVerfifiedAndConverted = true;
}
private void SortKeyFrameCues(Dictionary<double, (T, bool)> convertedValues)
private void AddNeutralKeyFramesIfNeeded()
{
bool hasStartKey, hasEndKey;
hasStartKey = hasEndKey = false;
@ -170,23 +148,20 @@ namespace Avalonia.Animation
}
if (!hasStartKey || !hasEndKey)
AddNeutralKeyFrames(hasStartKey, hasEndKey, _convertedKeyframes);
_convertedKeyframes = _convertedKeyframes.OrderBy(p => p.Key)
.ToDictionary((k) => k.Key, (v) => v.Value);
AddNeutralKeyFrames(hasStartKey, hasEndKey);
}
private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey, Dictionary<double, (T, bool)> convertedKeyframes)
private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey)
{
if (!hasStartKey)
{
convertedKeyframes.Add(0.0d, (default(T), true));
_convertedKeyframes.Add(0.0d, (new AnimatorKeyFrame { Value = default(T) }, true));
}
if (!hasEndKey)
{
convertedKeyframes.Add(1.0d, (default(T), true));
_convertedKeyframes.Add(1.0d, (new AnimatorKeyFrame { Value = default(T) }, true));
}
}
}
}
}

2
src/Avalonia.Animation/Cue.cs

@ -10,7 +10,7 @@ namespace Avalonia.Animation
/// A Cue object for <see cref="KeyFrame"/>.
/// </summary>
[TypeConverter(typeof(CueTypeConverter))]
public struct Cue : IEquatable<Cue>, IEquatable<double>
public readonly struct Cue : IEquatable<Cue>, IEquatable<double>
{
/// <summary>
/// The normalized percent value, ranging from 0.0 to 1.0

8
src/Avalonia.Animation/DoubleAnimator.cs

@ -24,15 +24,15 @@ namespace Avalonia.Animation
var firstKF = pair.KFPair.FirstKeyFrame;
var secondKF = pair.KFPair.SecondKeyFrame;
if (firstKF.Value.isNeutral)
if (firstKF.isNeutral)
y0 = neutralValue;
else
y0 = firstKF.Value.TargetValue;
y0 = firstKF.TargetValue;
if (secondKF.Value.isNeutral)
if (secondKF.isNeutral)
y1 = neutralValue;
else
y1 = secondKF.Value.TargetValue;
y1 = secondKF.TargetValue;
// Do linear parametric interpolation
return y0 + (pair.IntraKFTime) * (y1 - y0);

2
src/Avalonia.Animation/IAnimationSetter.cs

@ -5,4 +5,4 @@ namespace Avalonia.Animation
AvaloniaProperty Property { get; set; }
object Value { get; set; }
}
}
}

16
src/Avalonia.Animation/KeyFrame.cs

@ -7,6 +7,11 @@ using Avalonia.Collections;
namespace Avalonia.Animation
{
internal enum KeyFrameTimingMode
{
TimeSpan = 1,
Cue
}
/// <summary>
/// Stores data regarding a specific key
@ -14,7 +19,6 @@ namespace Avalonia.Animation
/// </summary>
public class KeyFrame : AvaloniaList<IAnimationSetter>
{
internal bool timeSpanSet, cueSet;
private TimeSpan _ktimeSpan;
private Cue _kCue;
@ -30,6 +34,8 @@ namespace Avalonia.Animation
{
}
internal KeyFrameTimingMode TimingMode { get; private set; }
/// <summary>
/// Gets or sets the key time of this <see cref="KeyFrame"/>.
/// </summary>
@ -42,11 +48,11 @@ namespace Avalonia.Animation
}
set
{
if (cueSet)
if (TimingMode == KeyFrameTimingMode.Cue)
{
throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}.");
}
timeSpanSet = true;
TimingMode = KeyFrameTimingMode.TimeSpan;
_ktimeSpan = value;
}
}
@ -63,11 +69,11 @@ namespace Avalonia.Animation
}
set
{
if (timeSpanSet)
if (TimingMode == KeyFrameTimingMode.TimeSpan)
{
throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}.");
}
cueSet = true;
TimingMode = KeyFrameTimingMode.Cue;
_kCue = value;
}
}

8
src/Avalonia.Animation/KeyFramePair`1.cs

@ -22,7 +22,7 @@ namespace Avalonia.Animation
/// </summary>
/// <param name="FirstKeyFrame"></param>
/// <param name="LastKeyFrame"></param>
public KeyFramePair(KeyValuePair<double, (T, bool)> FirstKeyFrame, KeyValuePair<double, (T, bool)> LastKeyFrame) : this()
public KeyFramePair((T TargetValue, bool isNeutral) FirstKeyFrame, (T TargetValue, bool isNeutral) LastKeyFrame) : this()
{
this.FirstKeyFrame = FirstKeyFrame;
this.SecondKeyFrame = LastKeyFrame;
@ -31,11 +31,11 @@ namespace Avalonia.Animation
/// <summary>
/// First <see cref="KeyFrame"/> object.
/// </summary>
public KeyValuePair<double, (T TargetValue, bool isNeutral)> FirstKeyFrame { get; private set; }
public (T TargetValue, bool isNeutral) FirstKeyFrame { get; }
/// <summary>
/// Second <see cref="KeyFrame"/> object.
/// </summary>
public KeyValuePair<double, (T TargetValue, bool isNeutral)> SecondKeyFrame { get; private set; }
public (T TargetValue, bool isNeutral) SecondKeyFrame { get; }
}
}
}

2
src/Avalonia.Base/AvaloniaObject.cs

@ -436,7 +436,7 @@ namespace Avalonia
/// Gets all priority values set on the object.
/// </summary>
/// <returns>A collection of property/value tuples.</returns>
internal IDictionary<AvaloniaProperty, PriorityValue> GetSetValues() => Values?.GetSetValues();
internal IDictionary<AvaloniaProperty, object> GetSetValues() => Values?.GetSetValues();
/// <summary>
/// Forces revalidation of properties when a property value changes.

52
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -106,7 +106,7 @@ namespace Avalonia
}
/// <summary>
/// Finds a registered non-attached property on a type by name.
/// Finds a registered property on a type by name.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="name">The property name.</param>
@ -130,7 +130,7 @@ namespace Avalonia
}
/// <summary>
/// Finds a registered non-attached property on a type by name.
/// Finds a registered property on an object by name.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="name">The property name.</param>
@ -148,52 +148,6 @@ namespace Avalonia
return FindRegistered(o.GetType(), name);
}
/// <summary>
/// Finds a registered attached property on a type by name.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="ownerType">The owner type.</param>
/// <param name="name">The property name.</param>
/// <returns>
/// The registered property or null if no matching property found.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The property name contains a '.'.
/// </exception>
public AvaloniaProperty FindRegisteredAttached(Type type, Type ownerType, string name)
{
Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(ownerType != null);
Contract.Requires<ArgumentNullException>(name != null);
if (name.Contains('.'))
{
throw new InvalidOperationException("Attached properties not supported.");
}
return GetRegisteredAttached(type).FirstOrDefault(x => x.Name == name);
}
/// <summary>
/// Finds a registered non-attached property on a type by name.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="ownerType">The owner type.</param>
/// <param name="name">The property name.</param>
/// <returns>
/// The registered property or null if no matching property found.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The property name contains a '.'.
/// </exception>
public AvaloniaProperty FindRegisteredAttached(AvaloniaObject o, Type ownerType, string name)
{
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(name != null);
return FindRegisteredAttached(o.GetType(), ownerType, name);
}
/// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is registered on a type.
/// </summary>
@ -287,4 +241,4 @@ namespace Avalonia
_attachedCache.Clear();
}
}
}
}

25
src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs

@ -23,15 +23,24 @@ namespace Avalonia.Diagnostics
{
var set = o.GetSetValues();
PriorityValue value;
if (set.TryGetValue(property, out value))
if (set.TryGetValue(property, out var obj))
{
return new AvaloniaPropertyValue(
property,
o.GetValue(property),
(BindingPriority)value.ValuePriority,
value.GetDiagnostic());
if (obj is PriorityValue value)
{
return new AvaloniaPropertyValue(
property,
o.GetValue(property),
(BindingPriority)value.ValuePriority,
value.GetDiagnostic());
}
else
{
return new AvaloniaPropertyValue(
property,
obj,
BindingPriority.LocalValue,
"Local value");
}
}
else
{

6
src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs → src/Avalonia.Base/Utilities/CharacterReader.cs

@ -3,14 +3,14 @@
using System;
namespace Avalonia.Markup.Parsers
namespace Avalonia.Utilities
{
internal class Reader
public class CharacterReader
{
private readonly string _s;
private int _i;
public Reader(string s)
public CharacterReader(string s)
{
_s = s;
}

6
src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs → src/Avalonia.Base/Utilities/IdentifierParser.cs

@ -4,11 +4,11 @@
using System.Globalization;
using System.Text;
namespace Avalonia.Markup.Parsers
namespace Avalonia.Utilities
{
internal static class IdentifierParser
public static class IdentifierParser
{
public static string Parse(Reader r)
public static string Parse(CharacterReader r)
{
if (IsValidIdentifierStart(r.Peek))
{

2
src/Avalonia.Base/ValueStore.cs

@ -100,7 +100,7 @@ namespace Avalonia
_owner.PriorityValueChanged(property, priority, oldValue, newValue);
}
public IDictionary<AvaloniaProperty, PriorityValue> GetSetValues() => throw new NotImplementedException();
public IDictionary<AvaloniaProperty, object> GetSetValues() => _values;
public object GetValue(AvaloniaProperty property)
{

9
src/Avalonia.Controls/Image.cs

@ -1,12 +1,11 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Media;
using Avalonia.Media.Imaging;
namespace Avalonia.Controls
{
{
/// <summary>
/// Displays a <see cref="Bitmap"/> image.
/// </summary>
@ -68,7 +67,9 @@ namespace Avalonia.Controls
Rect sourceRect = new Rect(sourceSize)
.CenterRect(new Rect(destRect.Size / scale));
context.DrawImage(source, 1, sourceRect, destRect);
var interpolationMode = RenderOptions.GetBitmapInterpolationMode(this);
context.DrawImage(source, 1, sourceRect, destRect, interpolationMode);
}
}
@ -100,4 +101,4 @@ namespace Avalonia.Controls
}
}
}
}
}

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

@ -189,6 +189,12 @@ namespace Avalonia.Controls.Presenters
_caretTimer.Start();
InvalidateVisual();
}
else
{
_caretTimer.Start();
InvalidateVisual();
_caretTimer.Stop();
}
if (IsMeasureValid)
{

2
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -376,7 +376,7 @@ namespace Avalonia.Controls.Primitives
break;
case NotifyCollectionChangedAction.Reset:
SelectedIndex = IndexOf(e.NewItems, SelectedItem);
SelectedIndex = IndexOf(Items, SelectedItem);
break;
}
}

90
src/Avalonia.Controls/ProgressBar.cs

@ -21,18 +21,27 @@ namespace Avalonia.Controls
public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<ProgressBar, Orientation>(nameof(Orientation), Orientation.Horizontal);
private static readonly DirectProperty<ProgressBar, double> IndeterminateStartingOffsetProperty =
AvaloniaProperty.RegisterDirect<ProgressBar, double>(
nameof(IndeterminateStartingOffset),
p => p.IndeterminateStartingOffset,
(p, o) => p.IndeterminateStartingOffset = o);
private static readonly DirectProperty<ProgressBar, double> IndeterminateEndingOffsetProperty =
AvaloniaProperty.RegisterDirect<ProgressBar, double>(
nameof(IndeterminateEndingOffset),
p => p.IndeterminateEndingOffset,
(p, o) => p.IndeterminateEndingOffset = o);
private Border _indicator;
private IndeterminateAnimation _indeterminateAnimation;
static ProgressBar()
{
PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical");
PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
PseudoClass(IsIndeterminateProperty, ":indeterminate");
ValueProperty.Changed.AddClassHandler<ProgressBar>(x => x.ValueChanged);
IsIndeterminateProperty.Changed.AddClassHandler<ProgressBar>(
(p, e) => { if (p._indicator != null) p.UpdateIsIndeterminate((bool)e.NewValue); });
}
public bool IsIndeterminate
@ -46,6 +55,19 @@ namespace Avalonia.Controls
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
private double _indeterminateStartingOffset;
private double IndeterminateStartingOffset
{
get => _indeterminateStartingOffset;
set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value);
}
private double _indeterminateEndingOffset;
private double IndeterminateEndingOffset
{
get => _indeterminateEndingOffset;
set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value);
}
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
@ -60,7 +82,6 @@ namespace Avalonia.Controls
_indicator = e.NameScope.Get<Border>("PART_Indicator");
UpdateIndicator(Bounds.Size);
UpdateIsIndeterminate(IsIndeterminate);
}
private void UpdateIndicator(Size bounds)
@ -70,9 +91,20 @@ namespace Avalonia.Controls
if (IsIndeterminate)
{
if (Orientation == Orientation.Horizontal)
_indicator.Width = bounds.Width / 5.0;
{
var width = bounds.Width / 5.0;
IndeterminateStartingOffset = -width;
_indicator.Width = width;
IndeterminateEndingOffset = bounds.Width;
}
else
_indicator.Height = bounds.Height / 5.0;
{
var height = bounds.Height / 5.0;
IndeterminateStartingOffset = -bounds.Height;
_indicator.Height = height;
IndeterminateEndingOffset = height;
}
}
else
{
@ -86,53 +118,9 @@ namespace Avalonia.Controls
}
}
private void UpdateIsIndeterminate(bool isIndeterminate)
{
if (isIndeterminate)
{
if (_indeterminateAnimation == null || _indeterminateAnimation.Disposed)
_indeterminateAnimation = IndeterminateAnimation.StartAnimation(this);
}
else
_indeterminateAnimation?.Dispose();
}
private void ValueChanged(AvaloniaPropertyChangedEventArgs e)
{
UpdateIndicator(Bounds.Size);
}
// TODO: Implement Indeterminate Progress animation
// in xaml (most ideal) or if it's not possible
// then on this class.
private class IndeterminateAnimation : IDisposable
{
private WeakReference<ProgressBar> _progressBar;
private bool _disposed;
public bool Disposed => _disposed;
private IndeterminateAnimation(ProgressBar progressBar)
{
_progressBar = new WeakReference<ProgressBar>(progressBar);
}
public static IndeterminateAnimation StartAnimation(ProgressBar progressBar)
{
return new IndeterminateAnimation(progressBar);
}
private Rect GetAnimationRect(TimeSpan time)
{
return Rect.Empty;
}
public void Dispose()
{
_disposed = true;
}
}
}
}

5
src/Avalonia.Controls/Slider.cs

@ -17,7 +17,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="Orientation"/> property.
/// </summary>
public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<Slider, Orientation>(nameof(Orientation), Orientation.Horizontal);
ScrollBar.OrientationProperty.AddOwner<Slider>();
/// <summary>
/// Defines the <see cref="IsSnapToTickEnabled"/> property.
@ -41,8 +41,7 @@ namespace Avalonia.Controls
/// </summary>
static Slider()
{
PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical");
PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal);
Thumb.DragStartedEvent.AddClassHandler<Slider>(x => x.OnThumbDragStarted, RoutingStrategies.Bubble);
Thumb.DragDeltaEvent.AddClassHandler<Slider>(x => x.OnThumbDragDelta, RoutingStrategies.Bubble);
Thumb.DragCompletedEvent.AddClassHandler<Slider>(x => x.OnThumbDragCompleted, RoutingStrategies.Bubble);

38
src/Avalonia.Controls/StackPanel.cs

@ -13,10 +13,10 @@ namespace Avalonia.Controls
public class StackPanel : Panel, INavigableContainer
{
/// <summary>
/// Defines the <see cref="Gap"/> property.
/// Defines the <see cref="Spacing"/> property.
/// </summary>
public static readonly StyledProperty<double> GapProperty =
AvaloniaProperty.Register<StackPanel, double>(nameof(Gap));
public static readonly StyledProperty<double> SpacingProperty =
AvaloniaProperty.Register<StackPanel, double>(nameof(Spacing));
/// <summary>
/// Defines the <see cref="Orientation"/> property.
@ -29,17 +29,17 @@ namespace Avalonia.Controls
/// </summary>
static StackPanel()
{
AffectsMeasure(GapProperty);
AffectsMeasure(SpacingProperty);
AffectsMeasure(OrientationProperty);
}
/// <summary>
/// Gets or sets the size of the gap to place between child controls.
/// Gets or sets the size of the spacing to place between child controls.
/// </summary>
public double Gap
public double Spacing
{
get { return GetValue(GapProperty); }
set { SetValue(GapProperty, value); }
get { return GetValue(SpacingProperty); }
set { SetValue(SpacingProperty, value); }
}
/// <summary>
@ -152,7 +152,7 @@ namespace Avalonia.Controls
double measuredWidth = 0;
double measuredHeight = 0;
double gap = Gap;
double spacing = Spacing;
bool hasVisibleChild = Children.Any(c => c.IsVisible);
foreach (Control child in Children)
@ -162,23 +162,23 @@ namespace Avalonia.Controls
if (Orientation == Orientation.Vertical)
{
measuredHeight += size.Height + (child.IsVisible ? gap : 0);
measuredHeight += size.Height + (child.IsVisible ? spacing : 0);
measuredWidth = Math.Max(measuredWidth, size.Width);
}
else
{
measuredWidth += size.Width + (child.IsVisible ? gap : 0);
measuredWidth += size.Width + (child.IsVisible ? spacing : 0);
measuredHeight = Math.Max(measuredHeight, size.Height);
}
}
if (Orientation == Orientation.Vertical)
{
measuredHeight -= (hasVisibleChild ? gap : 0);
measuredHeight -= (hasVisibleChild ? spacing : 0);
}
else
{
measuredWidth -= (hasVisibleChild ? gap : 0);
measuredWidth -= (hasVisibleChild ? spacing : 0);
}
return new Size(measuredWidth, measuredHeight);
@ -194,7 +194,7 @@ namespace Avalonia.Controls
var orientation = Orientation;
double arrangedWidth = finalSize.Width;
double arrangedHeight = finalSize.Height;
double gap = Gap;
double spacing = Spacing;
bool hasVisibleChild = Children.Any(c => c.IsVisible);
if (Orientation == Orientation.Vertical)
@ -217,25 +217,25 @@ namespace Avalonia.Controls
Rect childFinal = new Rect(0, arrangedHeight, width, childHeight);
ArrangeChild(child, childFinal, finalSize, orientation);
arrangedWidth = Math.Max(arrangedWidth, childWidth);
arrangedHeight += childHeight + (child.IsVisible ? gap : 0);
arrangedHeight += childHeight + (child.IsVisible ? spacing : 0);
}
else
{
double height = Math.Max(childHeight, arrangedHeight);
Rect childFinal = new Rect(arrangedWidth, 0, childWidth, height);
ArrangeChild(child, childFinal, finalSize, orientation);
arrangedWidth += childWidth + (child.IsVisible ? gap : 0);
arrangedWidth += childWidth + (child.IsVisible ? spacing : 0);
arrangedHeight = Math.Max(arrangedHeight, childHeight);
}
}
if (orientation == Orientation.Vertical)
{
arrangedHeight = Math.Max(arrangedHeight - (hasVisibleChild ? gap : 0), finalSize.Height);
arrangedHeight = Math.Max(arrangedHeight - (hasVisibleChild ? spacing : 0), finalSize.Height);
}
else
{
arrangedWidth = Math.Max(arrangedWidth - (hasVisibleChild ? gap : 0), finalSize.Width);
arrangedWidth = Math.Max(arrangedWidth - (hasVisibleChild ? spacing : 0), finalSize.Width);
}
return new Size(arrangedWidth, arrangedHeight);
@ -250,4 +250,4 @@ namespace Avalonia.Controls
child.Arrange(rect);
}
}
}
}

14
src/Avalonia.Controls/TextBox.cs

@ -124,7 +124,7 @@ namespace Avalonia.Controls
ScrollViewer.HorizontalScrollBarVisibilityProperty,
horizontalScrollBarVisibility,
BindingPriority.Style);
_undoRedoHelper = new UndoRedoHelper<UndoRedoState>(this);
_undoRedoHelper = new UndoRedoHelper<UndoRedoState>(this);
}
public bool AcceptsReturn
@ -262,7 +262,7 @@ namespace Avalonia.Controls
if (IsFocused)
{
_presenter.ShowCaret();
DecideCaretVisibility();
}
}
@ -282,12 +282,20 @@ namespace Avalonia.Controls
}
else
{
_presenter?.ShowCaret();
DecideCaretVisibility();
}
e.Handled = true;
}
private void DecideCaretVisibility()
{
if (!IsReadOnly)
_presenter?.ShowCaret();
else
_presenter?.HideCaret();
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);

12
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -200,7 +200,7 @@ namespace Avalonia.Controls
private void UpdateAdd(IControl child)
{
var bounds = Bounds;
var gap = Gap;
var spacing = Spacing;
child.Measure(_availableSpace);
++_averageCount;
@ -208,13 +208,13 @@ namespace Avalonia.Controls
if (Orientation == Orientation.Vertical)
{
var height = child.DesiredSize.Height;
_takenSpace += height + gap;
_takenSpace += height + spacing;
AddToAverageItemSize(height);
}
else
{
var width = child.DesiredSize.Width;
_takenSpace += width + gap;
_takenSpace += width + spacing;
AddToAverageItemSize(width);
}
}
@ -222,18 +222,18 @@ namespace Avalonia.Controls
private void UpdateRemove(IControl child)
{
var bounds = Bounds;
var gap = Gap;
var spacing = Spacing;
if (Orientation == Orientation.Vertical)
{
var height = child.DesiredSize.Height;
_takenSpace -= height + gap;
_takenSpace -= height + spacing;
RemoveFromAverageItemSize(height);
}
else
{
var width = child.DesiredSize.Width;
_takenSpace -= width + gap;
_takenSpace -= width + spacing;
RemoveFromAverageItemSize(width);
}

10
src/Avalonia.Controls/Window.cs

@ -302,17 +302,23 @@ namespace Avalonia.Controls
internal void Close(bool ignoreCancel)
{
bool close = true;
try
{
if (!ignoreCancel && HandleClosing())
{
close = false;
return;
}
}
finally
{
PlatformImpl?.Dispose();
HandleClosed();
if (close)
{
PlatformImpl?.Dispose();
HandleClosed();
}
}
}

4
src/Avalonia.Controls/WindowCollection.cs

@ -96,7 +96,7 @@ namespace Avalonia
{
while (_windows.Count > 0)
{
_windows[0].Close();
_windows[0].Close(true);
}
}
@ -131,4 +131,4 @@ namespace Avalonia
}
}
}
}
}

12
src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj

@ -1,7 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<!-- WARNING! The designer support version number needs to be frozen
To allow projects that implement designer functionality to still
work with newer versions of Avalonia. This version number only
need change when there are breaking changes to designer support api.
-->
<Version>0.7.0</Version>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols>
@ -37,11 +42,6 @@
<ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" />
<ProjectReference Include="..\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Shared\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
</ItemGroup>
<Import Project="..\..\build\Microsoft.CSharp.props" />
<Import Project="..\..\build\Rx.props" />
</Project>

4
src/Avalonia.Diagnostics/DevTools.xaml

@ -7,7 +7,7 @@
<ContentControl Content="{Binding Content}" Grid.Row="1"/>
<StackPanel Gap="4" Orientation="Horizontal" Grid.Row="2">
<StackPanel Spacing="4" Orientation="Horizontal" Grid.Row="2">
<TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
<Separator Width="8"/>
<TextBlock>Focused:</TextBlock>
@ -17,4 +17,4 @@
<TextBlock Text="{Binding PointerOverElement}"/>
</StackPanel>
</Grid>
</UserControl>
</UserControl>

1
src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs

@ -31,6 +31,7 @@ namespace Avalonia.Diagnostics.ViewModels
}
};
SelectedTab = 0;
root.GetObservable(TopLevel.PointerOverElementProperty)
.Subscribe(x => PointerOverElement = x?.GetType().Name);
}

4
src/Avalonia.Diagnostics/Views/TreePageView.xaml

@ -5,7 +5,7 @@
<TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:TreeNode"
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Gap="8">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{Binding Type}"/>
<TextBlock Text="{Binding Classes}"/>
</StackPanel>
@ -21,4 +21,4 @@
<GridSplitter Width="4" Grid.Column="1"/>
<ContentControl Content="{Binding Details}" Grid.Column="2"/>
</Grid>
</UserControl>
</UserControl>

6
src/Avalonia.Styling/Styling/Setter.cs

@ -126,7 +126,7 @@ namespace Avalonia.Styling
if (source != null)
{
var cloned = Clone(source, style, activator);
var cloned = Clone(source, source.Mode == BindingMode.Default ? Property.GetMetadata(control.GetType()).DefaultBindingMode : source.Mode, style, activator);
return BindingOperations.Apply(control, Property, cloned, null);
}
}
@ -134,13 +134,13 @@ namespace Avalonia.Styling
return Disposable.Empty;
}
private InstancedBinding Clone(InstancedBinding sourceInstance, IStyle style, IObservable<bool> activator)
private InstancedBinding Clone(InstancedBinding sourceInstance, BindingMode mode, IStyle style, IObservable<bool> activator)
{
if (activator != null)
{
var description = style?.ToString();
switch (sourceInstance.Mode)
switch (mode)
{
case BindingMode.OneTime:
if (sourceInstance.Observable != null)

45
src/Avalonia.Themes.Default/ProgressBar.xaml

@ -7,14 +7,9 @@
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Border Name="PART_Track"
BorderThickness="1"
BorderBrush="{TemplateBinding Background}"/>
<Border Name="PART_Indicator"
BorderThickness="1"
Background="{TemplateBinding Foreground}" />
</Grid>
<Border Name="PART_Indicator"
BorderThickness="1"
Background="{TemplateBinding Foreground}"/>
</Border>
</ControlTemplate>
</Setter>
@ -35,4 +30,36 @@
<Setter Property="MinWidth" Value="14"/>
<Setter Property="MinHeight" Value="200"/>
</Style>
</Styles>
<Style Selector="ProgressBar:horizontal:indeterminate /template/ Border#PART_Indicator">
<Style.Animations>
<Animation Duration="0:0:3"
RepeatCount="Loop"
Easing="LinearEasing">
<KeyFrame Cue="0%">
<Setter Property="TranslateTransform.X"
Value="{Binding IndeterminateStartingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="TranslateTransform.X"
Value="{Binding IndeterminateEndingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="ProgressBar:vertical:indeterminate /template/ Border#PART_Indicator">
<Style.Animations>
<Animation Duration="0:0:3"
RepeatCount="Loop"
Easing="LinearEasing">
<KeyFrame Cue="0%">
<Setter Property="TranslateTransform.Y"
Value="{Binding IndeterminateStartingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="TranslateTransform.Y"
Value="{Binding IndeterminateEndingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Styles>

10
src/Avalonia.Visuals/Media/DrawingContext.cs

@ -2,9 +2,10 @@ using System;
using System.Collections.Generic;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Visuals.Media.Imaging;
namespace Avalonia.Media
{
{
public sealed class DrawingContext : IDisposable
{
private int _currentLevel;
@ -68,11 +69,12 @@ namespace Avalonia.Media
/// <param name="opacity">The opacity to draw with.</param>
/// <param name="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</param>
public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect)
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default)
{
Contract.Requires<ArgumentNullException>(source != null);
PlatformImpl.DrawImage(source.PlatformImpl, opacity, sourceRect, destRect);
PlatformImpl.DrawImage(source.PlatformImpl, opacity, sourceRect, destRect, bitmapInterpolationMode);
}
/// <summary>
@ -309,4 +311,4 @@ namespace Avalonia.Media
return pen?.Brush != null && pen.Thickness > 0;
}
}
}
}

19
src/Avalonia.Visuals/Media/ITileBrush.cs

@ -1,5 +1,10 @@
namespace Avalonia.Media
{
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Visuals.Media.Imaging;
namespace Avalonia.Media
{
/// <summary>
/// A brush which displays a repeating image.
/// </summary>
@ -35,5 +40,13 @@
/// Gets the brush's tile mode.
/// </summary>
TileMode TileMode { get; }
/// <summary>
/// Gets the bitmap interpolation mode.
/// </summary>
/// <value>
/// The bitmap interpolation mode.
/// </value>
BitmapInterpolationMode BitmapInterpolationMode { get; }
}
}
}

31
src/Avalonia.Visuals/Media/Imaging/BitmapInterpolationMode.cs

@ -0,0 +1,31 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Visuals.Media.Imaging
{
/// <summary>
/// Controls the performance and quality of bitmap scaling.
/// </summary>
public enum BitmapInterpolationMode
{
/// <summary>
/// Uses the default behavior of the underling render backend.
/// </summary>
Default,
/// <summary>
/// The best performance but worst image quality.
/// </summary>
LowQuality,
/// <summary>
/// Good performance and decent image quality.
/// </summary>
MediumQuality,
/// <summary>
/// Highest quality but worst performance.
/// </summary>
HighQuality
}
}

11
src/Avalonia.Visuals/Media/Immutable/ImmutableImageBrush.cs

@ -1,5 +1,5 @@
using System;
using Avalonia.Media.Imaging;
using Avalonia.Media.Imaging;
using Avalonia.Visuals.Media.Imaging;
namespace Avalonia.Media.Immutable
{
@ -21,6 +21,7 @@ namespace Avalonia.Media.Immutable
/// How the source rectangle will be stretched to fill the destination rect.
/// </param>
/// <param name="tileMode">The tile mode.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
public ImmutableImageBrush(
IBitmap source,
AlignmentX alignmentX = AlignmentX.Center,
@ -29,7 +30,8 @@ namespace Avalonia.Media.Immutable
double opacity = 1,
RelativeRect? sourceRect = null,
Stretch stretch = Stretch.Uniform,
TileMode tileMode = TileMode.None)
TileMode tileMode = TileMode.None,
BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
: base(
alignmentX,
alignmentY,
@ -37,7 +39,8 @@ namespace Avalonia.Media.Immutable
opacity,
sourceRect ?? RelativeRect.Fill,
stretch,
tileMode)
tileMode,
bitmapInterpolationMode)
{
Source = source;
}

16
src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs

@ -1,4 +1,7 @@
using System;
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Visuals.Media.Imaging;
namespace Avalonia.Media.Immutable
{
@ -19,6 +22,7 @@ namespace Avalonia.Media.Immutable
/// How the source rectangle will be stretched to fill the destination rect.
/// </param>
/// <param name="tileMode">The tile mode.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
protected ImmutableTileBrush(
AlignmentX alignmentX,
AlignmentY alignmentY,
@ -26,7 +30,8 @@ namespace Avalonia.Media.Immutable
double opacity,
RelativeRect sourceRect,
Stretch stretch,
TileMode tileMode)
TileMode tileMode,
BitmapInterpolationMode bitmapInterpolationMode)
{
AlignmentX = alignmentX;
AlignmentY = alignmentY;
@ -35,6 +40,7 @@ namespace Avalonia.Media.Immutable
SourceRect = sourceRect;
Stretch = stretch;
TileMode = tileMode;
BitmapInterpolationMode = bitmapInterpolationMode;
}
/// <summary>
@ -49,7 +55,8 @@ namespace Avalonia.Media.Immutable
source.Opacity,
source.SourceRect,
source.Stretch,
source.TileMode)
source.TileMode,
source.BitmapInterpolationMode)
{
}
@ -73,5 +80,8 @@ namespace Avalonia.Media.Immutable
/// <inheritdoc/>
public TileMode TileMode { get; }
/// <inheritdoc/>
public BitmapInterpolationMode BitmapInterpolationMode { get; }
}
}

12
src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs

@ -1,4 +1,7 @@
using System;
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Visuals.Media.Imaging;
using Avalonia.VisualTree;
namespace Avalonia.Media.Immutable
@ -21,6 +24,7 @@ namespace Avalonia.Media.Immutable
/// How the source rectangle will be stretched to fill the destination rect.
/// </param>
/// <param name="tileMode">The tile mode.</param>
/// <param name="bitmapInterpolationMode">Controls the quality of interpolation.</param>
public ImmutableVisualBrush(
IVisual visual,
AlignmentX alignmentX = AlignmentX.Center,
@ -29,7 +33,8 @@ namespace Avalonia.Media.Immutable
double opacity = 1,
RelativeRect? sourceRect = null,
Stretch stretch = Stretch.Uniform,
TileMode tileMode = TileMode.None)
TileMode tileMode = TileMode.None,
BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
: base(
alignmentX,
alignmentY,
@ -37,7 +42,8 @@ namespace Avalonia.Media.Immutable
opacity,
sourceRect ?? RelativeRect.Fill,
stretch,
tileMode)
tileMode,
bitmapInterpolationMode)
{
Visual = visual;
}

39
src/Avalonia.Visuals/Media/RenderOptions.cs

@ -0,0 +1,39 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Visuals.Media.Imaging;
namespace Avalonia.Media
{
public class RenderOptions
{
/// <summary>
/// Defines the <see cref="BitmapInterpolationMode"/> property.
/// </summary>
public static readonly StyledProperty<BitmapInterpolationMode> BitmapInterpolationModeProperty =
AvaloniaProperty.RegisterAttached<RenderOptions, AvaloniaObject, BitmapInterpolationMode>(
"BitmapInterpolationMode",
BitmapInterpolationMode.MediumQuality,
inherits: true);
/// <summary>
/// Gets the value of the BitmapInterpolationMode attached property for a control.
/// </summary>
/// <param name="element">The control.</param>
/// <returns>The control's left coordinate.</returns>
public static BitmapInterpolationMode GetBitmapInterpolationMode(AvaloniaObject element)
{
return element.GetValue(BitmapInterpolationModeProperty);
}
/// <summary>
/// Sets the value of the BitmapInterpolationMode attached property for a control.
/// </summary>
/// <param name="element">The control.</param>
/// <param name="value">The left value.</param>
public static void SetBitmapInterpolationMode(AvaloniaObject element, BitmapInterpolationMode value)
{
element.SetValue(BitmapInterpolationModeProperty, value);
}
}
}

19
src/Avalonia.Visuals/Media/TileBrush.cs

@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Visuals.Media.Imaging;
namespace Avalonia.Media
{
/// <summary>
@ -75,6 +77,11 @@ namespace Avalonia.Media
public static readonly StyledProperty<TileMode> TileModeProperty =
AvaloniaProperty.Register<TileBrush, TileMode>(nameof(TileMode));
static TileBrush()
{
RenderOptions.BitmapInterpolationModeProperty.OverrideDefaultValue<TileBrush>(BitmapInterpolationMode.Default);
}
/// <summary>
/// Gets or sets the horizontal alignment of a tile in the destination.
/// </summary>
@ -129,5 +136,17 @@ namespace Avalonia.Media
get { return (TileMode)GetValue(TileModeProperty); }
set { SetValue(TileModeProperty, value); }
}
/// <summary>
/// Gets or sets the bitmap interpolation mode.
/// </summary>
/// <value>
/// The bitmap interpolation mode.
/// </value>
public BitmapInterpolationMode BitmapInterpolationMode
{
get { return RenderOptions.GetBitmapInterpolationMode(this); }
set { RenderOptions.SetBitmapInterpolationMode(this, value); }
}
}
}

4
src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs

@ -4,6 +4,7 @@
using System;
using Avalonia.Media;
using Avalonia.Utilities;
using Avalonia.Visuals.Media.Imaging;
namespace Avalonia.Platform
{
@ -30,7 +31,8 @@ namespace Avalonia.Platform
/// <param name="opacity">The opacity to draw with.</param>
/// <param name="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</param>
void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect);
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default);
/// <summary>
/// Draws a bitmap image.

7
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@ -7,6 +7,7 @@ using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Utilities;
using Avalonia.VisualTree;
using Avalonia.Visuals.Media.Imaging;
namespace Avalonia.Rendering.SceneGraph
{
@ -114,13 +115,13 @@ namespace Avalonia.Rendering.SceneGraph
}
/// <inheritdoc/>
public void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
public void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
{
var next = NextDrawAs<ImageNode>();
if (next == null || !next.Item.Equals(Transform, source, opacity, sourceRect, destRect))
if (next == null || !next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode))
{
Add(new ImageNode(Transform, source, opacity, sourceRect, destRect));
Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode));
}
else
{

23
src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs

@ -4,6 +4,7 @@
using System;
using Avalonia.Platform;
using Avalonia.Utilities;
using Avalonia.Visuals.Media.Imaging;
namespace Avalonia.Rendering.SceneGraph
{
@ -20,7 +21,8 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="opacity">The draw opacity.</param>
/// <param name="sourceRect">The source rect.</param>
/// <param name="destRect">The destination rect.</param>
public ImageNode(Matrix transform, IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
public ImageNode(Matrix transform, IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
: base(destRect, transform, null)
{
Transform = transform;
@ -28,7 +30,8 @@ namespace Avalonia.Rendering.SceneGraph
Opacity = opacity;
SourceRect = sourceRect;
DestRect = destRect;
}
BitmapInterpolationMode = bitmapInterpolationMode;
}
/// <summary>
/// Gets the transform with which the node will be drawn.
@ -55,6 +58,14 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
public Rect DestRect { get; }
/// <summary>
/// Gets the bitmap interpolation mode.
/// </summary>
/// <value>
/// The scaling mode.
/// </value>
public BitmapInterpolationMode BitmapInterpolationMode { get; }
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>
@ -63,18 +74,20 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="opacity">The opacity of the other draw operation.</param>
/// <param name="sourceRect">The source rect of the other draw operation.</param>
/// <param name="destRect">The dest rect of the other draw operation.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
public bool Equals(Matrix transform, IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
public bool Equals(Matrix transform, IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
{
return transform == Transform &&
Equals(source.Item, Source.Item) &&
opacity == Opacity &&
sourceRect == SourceRect &&
destRect == DestRect;
destRect == DestRect &&
bitmapInterpolationMode == BitmapInterpolationMode;
}
/// <inheritdoc/>
@ -83,7 +96,7 @@ namespace Avalonia.Rendering.SceneGraph
// TODO: Probably need to introduce some kind of locking mechanism in the case of
// WriteableBitmap.
context.Transform = Transform;
context.DrawImage(Source, Opacity, SourceRect, DestRect);
context.DrawImage(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode);
}
/// <inheritdoc/>

7
src/Gtk/Avalonia.Gtk3/KeyTransform.cs

@ -33,17 +33,24 @@ namespace Avalonia.Gtk.Common
{ GdkKey.Prior, Key.Prior },
//{ GdkKey.?, Key.PageDown }
{ GdkKey.End, Key.End },
{ GdkKey.KP_End, Key.End },
{ GdkKey.Home, Key.Home },
{ GdkKey.KP_Home, Key.Home },
{ GdkKey.Left, Key.Left },
{ GdkKey.KP_Left, Key.Left },
{ GdkKey.Up, Key.Up },
{ GdkKey.KP_Up, Key.Up },
{ GdkKey.Right, Key.Right },
{ GdkKey.KP_Right, Key.Right },
{ GdkKey.Down, Key.Down },
{ GdkKey.KP_Down, Key.Down },
{ GdkKey.Select, Key.Select },
{ GdkKey.Print, Key.Print },
{ GdkKey.Execute, Key.Execute },
//{ GdkKey.?, Key.Snapshot }
{ GdkKey.Insert, Key.Insert },
{ GdkKey.Delete, Key.Delete },
{ GdkKey.KP_Delete, Key.Delete },
{ GdkKey.Help, Key.Help },
//{ GdkKey.?, Key.D0 }
//{ GdkKey.?, Key.D1 }

1
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -33,6 +33,7 @@
<Compile Include="MarkupExtensions\ResourceInclude.cs" />
<Compile Include="MarkupExtensions\StaticResourceExtension.cs" />
<Compile Include="MarkupExtensions\StyleIncludeExtension.cs" />
<Compile Include="Parsers\PropertyParser.cs" />
<Compile Include="PortableXaml\AvaloniaXamlContext.cs" />
<Compile Include="PortableXaml\AttributeExtensions.cs" />
<Compile Include="PortableXaml\AvaloniaMemberAttributeProvider.cs" />

69
src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs

@ -4,19 +4,19 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Text.RegularExpressions;
using Avalonia.Controls;
using Avalonia.Logging;
using Avalonia.Markup.Parsers;
using Avalonia.Markup.Xaml.Parsers;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Styling;
using Portable.Xaml;
using Avalonia.Utilities;
using Portable.Xaml.ComponentModel;
using Portable.Xaml.Markup;
namespace Avalonia.Markup.Xaml.Converters
{
public class AvaloniaPropertyTypeConverter : TypeConverter
{
private static readonly Regex regex = new Regex(@"^\(?(\w*)\.(\w*)\)?|(.*)$");
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
@ -24,39 +24,48 @@ namespace Avalonia.Markup.Xaml.Converters
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var (owner, propertyName) = ParseProperty((string)value);
var ownerType = TryResolveOwnerByName(context, owner) ??
context.GetFirstAmbientValue<ControlTemplate>()?.TargetType ??
context.GetFirstAmbientValue<Style>()?.Selector?.TargetType;
var registry = AvaloniaPropertyRegistry.Instance;
var parser = new PropertyParser();
var reader = new CharacterReader((string)value);
var (ns, owner, propertyName) = parser.Parse(reader);
var ownerType = TryResolveOwnerByName(context, ns, owner);
var targetType = context.GetFirstAmbientValue<ControlTemplate>()?.TargetType ??
context.GetFirstAmbientValue<Style>()?.Selector?.TargetType ??
typeof(Control);
var effectiveOwner = ownerType ?? targetType;
var property = registry.FindRegistered(effectiveOwner, propertyName);
if (ownerType == null)
if (property == null)
{
throw new XamlLoadException(
$"Could not determine the owner type for property '{propertyName}'. " +
"Please fully qualify the property name or specify a target type on " +
"the containing template.");
throw new XamlLoadException($"Could not find property '{effectiveOwner.Name}.{propertyName}'.");
}
var property = AvaloniaPropertyRegistry.Instance.FindRegistered(ownerType, propertyName);
if (property == null)
if (effectiveOwner != targetType &&
!property.IsAttached &&
!registry.IsRegistered(targetType, property))
{
throw new XamlLoadException($"Could not find AvaloniaProperty '{ownerType.Name}.{propertyName}'.");
Logger.Warning(
LogArea.Property,
this,
"Property '{Owner}.{Name}' is not registered on '{Type}'.",
effectiveOwner,
propertyName,
targetType);
}
return property;
}
private Type TryResolveOwnerByName(ITypeDescriptorContext context, string owner)
private Type TryResolveOwnerByName(ITypeDescriptorContext context, string ns, string owner)
{
if (owner != null)
{
var resolver = context.GetService<IXamlTypeResolver>();
var result = resolver.Resolve(owner);
var result = context.ResolveType(ns, owner);
if (result == null)
{
throw new XamlLoadException($"Could not find type '{owner}'.");
var name = string.IsNullOrEmpty(ns) ? owner : $"{ns}:{owner}";
throw new XamlLoadException($"Could not find type '{name}'.");
}
return result;
@ -64,19 +73,5 @@ namespace Avalonia.Markup.Xaml.Converters
return null;
}
private (string owner, string property) ParseProperty(string s)
{
var result = regex.Match(s);
if (result.Groups[1].Success)
{
return (result.Groups[1].Value, result.Groups[2].Value);
}
else
{
return (null, result.Groups[3].Value);
}
}
}
}
}

160
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@ -32,176 +32,28 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
{
var descriptorContext = (ITypeDescriptorContext)serviceProvider;
var pathInfo = ParsePath(Path, descriptorContext);
ValidateState(pathInfo);
return new Binding
{
TypeResolver = descriptorContext.ResolveType,
Converter = Converter,
ConverterParameter = ConverterParameter,
ElementName = pathInfo.ElementName ?? ElementName,
ElementName = ElementName,
FallbackValue = FallbackValue,
Mode = Mode,
Path = pathInfo.Path,
Path = Path,
Priority = Priority,
Source = Source,
RelativeSource = pathInfo.RelativeSource ?? RelativeSource,
RelativeSource = RelativeSource,
DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)serviceProvider))
};
}
private class PathInfo
{
public string Path { get; set; }
public string ElementName { get; set; }
public RelativeSource RelativeSource { get; set; }
}
private void ValidateState(PathInfo pathInfo)
{
if (pathInfo.ElementName != null && ElementName != null)
{
throw new InvalidOperationException(
"ElementName property cannot be set when an #elementName path is provided.");
}
if (pathInfo.RelativeSource != null && RelativeSource != null)
{
throw new InvalidOperationException(
"ElementName property cannot be set when a $self or $parent path is provided.");
}
if ((pathInfo.ElementName != null || ElementName != null) &&
(pathInfo.RelativeSource != null || RelativeSource != null))
{
throw new InvalidOperationException(
"ElementName property cannot be set with a RelativeSource.");
}
}
private static PathInfo ParsePath(string path, ITypeDescriptorContext context)
{
var result = new PathInfo();
if (string.IsNullOrWhiteSpace(path) || path == ".")
{
result.Path = string.Empty;
}
else if (path.StartsWith("#"))
{
var dot = path.IndexOf('.');
if (dot != -1)
{
result.Path = path.Substring(dot + 1);
result.ElementName = path.Substring(1, dot - 1);
}
else
{
result.Path = string.Empty;
result.ElementName = path.Substring(1);
}
}
else if (path.StartsWith("$"))
{
var relativeSource = new RelativeSource
{
Tree = TreeType.Logical
};
result.RelativeSource = relativeSource;
var dot = path.IndexOf('.');
string relativeSourceMode;
if (dot != -1)
{
result.Path = path.Substring(dot + 1);
relativeSourceMode = path.Substring(1, dot - 1);
}
else
{
result.Path = string.Empty;
relativeSourceMode = path.Substring(1);
}
if (relativeSourceMode == "self")
{
relativeSource.Mode = RelativeSourceMode.Self;
}
else if (relativeSourceMode == "parent")
{
relativeSource.Mode = RelativeSourceMode.FindAncestor;
relativeSource.AncestorLevel = 1;
}
else if (relativeSourceMode.StartsWith("parent["))
{
relativeSource.Mode = RelativeSourceMode.FindAncestor;
var parentConfigStart = relativeSourceMode.IndexOf('[');
if (!relativeSourceMode.EndsWith("]"))
{
throw new InvalidOperationException("Invalid RelativeSource binding syntax. Expected matching ']' for '['.");
}
var parentConfigParams = relativeSourceMode.Substring(parentConfigStart + 1).TrimEnd(']').Split(';');
if (parentConfigParams.Length > 2 || parentConfigParams.Length == 0)
{
throw new InvalidOperationException("Expected either 1 or 2 parameters for RelativeSource binding syntax");
}
else if (parentConfigParams.Length == 1)
{
if (int.TryParse(parentConfigParams[0], out int level))
{
relativeSource.AncestorType = null;
relativeSource.AncestorLevel = level + 1;
}
else
{
relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0], context);
}
}
else
{
relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0], context);
relativeSource.AncestorLevel = int.Parse(parentConfigParams[1]) + 1;
}
}
else
{
throw new InvalidOperationException($"Invalid RelativeSource binding syntax: {relativeSourceMode}");
}
}
else
{
result.Path = path;
}
return result;
}
private static Type LookupAncestorType(string ancestorTypeName, ITypeDescriptorContext context)
{
var parts = ancestorTypeName.Split(':');
if (parts.Length == 0 || parts.Length > 2)
{
throw new InvalidOperationException("Invalid type name");
}
if (parts.Length == 1)
{
return context.ResolveType(string.Empty, parts[0]);
}
else
{
return context.ResolveType(parts[0], parts[1]);
}
}
private static object GetDefaultAnchor(ITypeDescriptorContext context)
{
object anchor = null;
// The target is not a control, so we need to find an anchor that will let us look
// If the target is not a control, so we need to find an anchor that will let us look
// up named controls and style resources. First look for the closest IControl in
// the context.
anchor = context.GetFirstAmbientValue<IControl>();
object anchor = context.GetFirstAmbientValue<IControl>();
// If a control was not found, then try to find the highest-level style as the XAML
// file could be a XAML file containing only styles.
@ -229,4 +81,4 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
public RelativeSource RelativeSource { get; set; }
}
}
}

85
src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs

@ -0,0 +1,85 @@
using System;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.Utilities;
namespace Avalonia.Markup.Xaml.Parsers
{
internal class PropertyParser
{
public (string ns, string owner, string name) Parse(CharacterReader r)
{
if (r.End)
{
throw new ExpressionParseException(0, "Expected property name.");
}
var openParens = r.TakeIf('(');
bool closeParens = false;
string ns = null;
string owner = null;
string name = null;
do
{
var token = IdentifierParser.Parse(r);
if (token == null)
{
if (r.End)
{
break;
}
else
{
if (openParens && !r.End && (closeParens = r.TakeIf(')')))
{
break;
}
else if (openParens)
{
throw new ExpressionParseException(r.Position, $"Expected ')'.");
}
throw new ExpressionParseException(r.Position, $"Unexpected '{r.Peek}'.");
}
}
else if (!r.End && r.TakeIf(':'))
{
ns = ns == null ?
token :
throw new ExpressionParseException(r.Position, "Unexpected ':'.");
}
else if (!r.End && r.TakeIf('.'))
{
owner = owner == null ?
token :
throw new ExpressionParseException(r.Position, "Unexpected '.'.");
}
else
{
name = token;
}
} while (!r.End);
if (name == null)
{
throw new ExpressionParseException(0, "Expected property name.");
}
else if (openParens && owner == null)
{
throw new ExpressionParseException(1, "Expected property owner.");
}
else if (openParens && !closeParens)
{
throw new ExpressionParseException(r.Position, "Expected ')'.");
}
else if (!r.End)
{
throw new ExpressionParseException(r.Position, "Expected end of expression.");
}
return (ns, owner, name);
}
}
}

4
src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaRuntimeTypeProvider.cs

@ -7,7 +7,6 @@ using System.Linq;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Markup.Data;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
using Avalonia.Metadata;
@ -33,6 +32,7 @@ namespace Avalonia.Markup.Xaml.Context
private static readonly IEnumerable<Assembly> ForcedAssemblies = new[]
{
typeof(AvaloniaObject).GetTypeInfo().Assembly,
typeof(Animation.Animation).GetTypeInfo().Assembly,
typeof(Control).GetTypeInfo().Assembly,
typeof(Style).GetTypeInfo().Assembly,
typeof(DataTemplate).GetTypeInfo().Assembly,
@ -146,4 +146,4 @@ namespace Avalonia.Markup.Xaml.Context
return null;
}
}
}
}

98
src/Markup/Avalonia.Markup/Data/Binding.cs

@ -105,34 +105,53 @@ namespace Avalonia.Data
ExpressionObserver observer;
var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver);
if (ElementName != null)
{
observer = CreateElementObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
ElementName,
Path,
enableDataValidation);
node);
}
else if (Source != null)
{
observer = CreateSourceObserver(Source, Path, enableDataValidation);
observer = CreateSourceObserver(Source, node);
}
else if (RelativeSource == null)
{
if (mode == SourceMode.Data)
{
observer = CreateDataContextObserver(
target,
node,
targetProperty == StyledElement.DataContextProperty,
anchor);
}
else
{
observer = new ExpressionObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
node);
}
}
else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext)
else if (RelativeSource.Mode == RelativeSourceMode.DataContext)
{
observer = CreateDataContextObserver(
target,
Path,
node,
targetProperty == StyledElement.DataContextProperty,
anchor,
enableDataValidation);
anchor);
}
else if (RelativeSource.Mode == RelativeSourceMode.Self)
{
observer = CreateSourceObserver(target, Path, enableDataValidation);
observer = CreateSourceObserver(target, node);
}
else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent)
{
observer = CreateTemplatedParentObserver(target, Path, enableDataValidation);
observer = CreateTemplatedParentObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
node);
}
else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor)
{
@ -144,8 +163,7 @@ namespace Avalonia.Data
observer = CreateFindAncestorObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
RelativeSource,
Path,
enableDataValidation);
node);
}
else
{
@ -176,10 +194,9 @@ namespace Avalonia.Data
private ExpressionObserver CreateDataContextObserver(
IAvaloniaObject target,
string path,
ExpressionNode node,
bool targetIsDataContext,
object anchor,
bool enableDataValidation)
object anchor)
{
Contract.Requires<ArgumentNullException>(target != null);
@ -195,48 +212,41 @@ namespace Avalonia.Data
if (!targetIsDataContext)
{
var result = ExpressionObserverBuilder.Build(
var result = new ExpressionObserver(
() => target.GetValue(StyledElement.DataContextProperty),
path,
node,
new UpdateSignal(target, StyledElement.DataContextProperty),
enableDataValidation,
typeResolver: TypeResolver);
null);
return result;
}
else
{
return ExpressionObserverBuilder.Build(
return new ExpressionObserver(
GetParentDataContext(target),
path,
enableDataValidation,
typeResolver: TypeResolver);
node,
null);
}
}
private ExpressionObserver CreateElementObserver(
IStyledElement target,
string elementName,
string path,
bool enableDataValidation)
ExpressionNode node)
{
Contract.Requires<ArgumentNullException>(target != null);
var description = $"#{elementName}.{path}";
var result = ExpressionObserverBuilder.Build(
var result = new ExpressionObserver(
ControlLocator.Track(target, elementName),
path,
enableDataValidation,
description,
typeResolver: TypeResolver);
node,
null);
return result;
}
private ExpressionObserver CreateFindAncestorObserver(
IStyledElement target,
RelativeSource relativeSource,
string path,
bool enableDataValidation)
ExpressionNode node)
{
Contract.Requires<ArgumentNullException>(target != null);
@ -260,36 +270,32 @@ namespace Avalonia.Data
throw new InvalidOperationException("Invalid tree to traverse.");
}
return ExpressionObserverBuilder.Build(
return new ExpressionObserver(
controlLocator,
path,
enableDataValidation,
typeResolver: TypeResolver);
node,
null);
}
private ExpressionObserver CreateSourceObserver(
object source,
string path,
bool enableDataValidation)
ExpressionNode node)
{
Contract.Requires<ArgumentNullException>(source != null);
return ExpressionObserverBuilder.Build(source, path, enableDataValidation, typeResolver: TypeResolver);
return new ExpressionObserver(source, node);
}
private ExpressionObserver CreateTemplatedParentObserver(
IAvaloniaObject target,
string path,
bool enableDataValidation)
ExpressionNode node)
{
Contract.Requires<ArgumentNullException>(target != null);
var result = ExpressionObserverBuilder.Build(
var result = new ExpressionObserver(
() => target.GetValue(StyledElement.TemplatedParentProperty),
path,
node,
new UpdateSignal(target, StyledElement.TemplatedParentProperty),
enableDataValidation,
typeResolver: TypeResolver);
null);
return result;
}

11
src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Data.Core;
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
using System.Text;
@ -10,7 +11,7 @@ namespace Avalonia.Markup.Parsers
{
internal static class ArgumentListParser
{
public static IList<string> Parse(Reader r, char open, char close)
public static IList<string> Parse(CharacterReader r, char open, char close, char delimiter = ',')
{
if (r.Peek == open)
{
@ -21,7 +22,7 @@ namespace Avalonia.Markup.Parsers
while (!r.End)
{
var builder = new StringBuilder();
while (!r.End && r.Peek != ',' && r.Peek != close && !char.IsWhiteSpace(r.Peek))
while (!r.End && r.Peek != delimiter && r.Peek != close && !char.IsWhiteSpace(r.Peek))
{
builder.Append(r.Take());
}
@ -35,7 +36,7 @@ namespace Avalonia.Markup.Parsers
if (r.End)
{
throw new ExpressionParseException(r.Position, "Expected ','.");
throw new ExpressionParseException(r.Position, $"Expected '{delimiter}'.");
}
else if (r.TakeIf(close))
{
@ -43,9 +44,9 @@ namespace Avalonia.Markup.Parsers
}
else
{
if (r.Take() != ',')
if (r.Take() != delimiter)
{
throw new ExpressionParseException(r.Position, "Expected ','.");
throw new ExpressionParseException(r.Position, $"Expected '{delimiter}'.");
}
r.SkipWhitespace();

15
src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs

@ -1,4 +1,5 @@
using Avalonia.Data.Core;
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
using System.Reactive;
@ -8,14 +9,14 @@ namespace Avalonia.Markup.Parsers
{
public static class ExpressionObserverBuilder
{
internal static ExpressionNode Parse(string expression, bool enableValidation = false, Func<string, string, Type> typeResolver = null)
internal static (ExpressionNode Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func<string, string, Type> typeResolver = null)
{
if (string.IsNullOrWhiteSpace(expression))
{
return new EmptyExpressionNode();
return (new EmptyExpressionNode(), default);
}
var reader = new Reader(expression);
var reader = new CharacterReader(expression);
var parser = new ExpressionParser(enableValidation, typeResolver);
var node = parser.Parse(reader);
@ -36,7 +37,7 @@ namespace Avalonia.Markup.Parsers
{
return new ExpressionObserver(
root,
Parse(expression, enableDataValidation, typeResolver),
Parse(expression, enableDataValidation, typeResolver).Node,
description ?? expression);
}
@ -50,7 +51,7 @@ namespace Avalonia.Markup.Parsers
Contract.Requires<ArgumentNullException>(rootObservable != null);
return new ExpressionObserver(
rootObservable,
Parse(expression, enableDataValidation, typeResolver),
Parse(expression, enableDataValidation, typeResolver).Node,
description ?? expression);
}
@ -66,8 +67,8 @@ namespace Avalonia.Markup.Parsers
Contract.Requires<ArgumentNullException>(rootGetter != null);
return new ExpressionObserver(
() => rootGetter(),
Parse(expression, enableDataValidation, typeResolver),
rootGetter,
Parse(expression, enableDataValidation, typeResolver).Node,
update,
description ?? expression);
}

158
src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs

@ -3,12 +3,19 @@
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers.Nodes;
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Markup.Parsers
{
internal enum SourceMode
{
Data,
Control
}
internal class ExpressionParser
{
private readonly bool _enableValidation;
@ -20,10 +27,11 @@ namespace Avalonia.Markup.Parsers
_enableValidation = enableValidation;
}
public ExpressionNode Parse(Reader r)
public (ExpressionNode Node, SourceMode Mode) Parse(CharacterReader r)
{
var nodes = new List<ExpressionNode>();
var state = State.Start;
var mode = SourceMode.Data;
while (!r.End && state != State.End)
{
@ -48,6 +56,16 @@ namespace Avalonia.Markup.Parsers
case State.Indexer:
state = ParseIndexer(r, nodes);
break;
case State.ElementName:
state = ParseElementName(r, nodes);
mode = SourceMode.Control;
break;
case State.RelativeSource:
state = ParseRelativeSource(r, nodes);
mode = SourceMode.Control;
break;
}
}
@ -61,16 +79,24 @@ namespace Avalonia.Markup.Parsers
nodes[n].Next = nodes[n + 1];
}
return nodes.FirstOrDefault();
return (nodes.FirstOrDefault(), mode);
}
private State ParseStart(Reader r, IList<ExpressionNode> nodes)
private State ParseStart(CharacterReader r, IList<ExpressionNode> nodes)
{
if (ParseNot(r))
{
nodes.Add(new LogicalNotNode());
return State.Start;
}
else if (ParseSharp(r))
{
return State.ElementName;
}
else if (ParseDollarSign(r))
{
return State.RelativeSource;
}
else if (ParseOpenBrace(r))
{
return State.AttachedProperty;
@ -93,7 +119,7 @@ namespace Avalonia.Markup.Parsers
return State.End;
}
private static State ParseAfterMember(Reader r, IList<ExpressionNode> nodes)
private static State ParseAfterMember(CharacterReader r, IList<ExpressionNode> nodes)
{
if (ParseMemberAccessor(r))
{
@ -112,7 +138,7 @@ namespace Avalonia.Markup.Parsers
return State.End;
}
private State ParseBeforeMember(Reader r, IList<ExpressionNode> nodes)
private State ParseBeforeMember(CharacterReader r, IList<ExpressionNode> nodes)
{
if (ParseOpenBrace(r))
{
@ -132,21 +158,9 @@ namespace Avalonia.Markup.Parsers
}
}
private State ParseAttachedProperty(Reader r, List<ExpressionNode> nodes)
private State ParseAttachedProperty(CharacterReader r, List<ExpressionNode> nodes)
{
string ns = string.Empty;
string owner;
var ownerOrNamespace = IdentifierParser.Parse(r);
if (r.TakeIf(':'))
{
ns = ownerOrNamespace;
owner = IdentifierParser.Parse(r);
}
else
{
owner = ownerOrNamespace;
}
var (ns, owner) = ParseTypeName(r);
if (r.End || !r.TakeIf('.'))
{
@ -171,7 +185,7 @@ namespace Avalonia.Markup.Parsers
return State.AfterMember;
}
private State ParseIndexer(Reader r, List<ExpressionNode> nodes)
private State ParseIndexer(CharacterReader r, List<ExpressionNode> nodes)
{
var args = ArgumentListParser.Parse(r, '[', ']');
@ -184,34 +198,128 @@ namespace Avalonia.Markup.Parsers
return State.AfterMember;
}
private static bool ParseNot(Reader r)
private State ParseElementName(CharacterReader r, List<ExpressionNode> nodes)
{
var name = IdentifierParser.Parse(r);
if (name == null)
{
throw new ExpressionParseException(r.Position, "Element name expected after '#'.");
}
nodes.Add(new ElementNameNode(name));
return State.AfterMember;
}
private State ParseRelativeSource(CharacterReader r, List<ExpressionNode> nodes)
{
var mode = IdentifierParser.Parse(r);
if (mode == "self")
{
nodes.Add(new SelfNode());
}
else if (mode == "parent")
{
Type ancestorType = null;
var ancestorLevel = 0;
if (PeekOpenBracket(r))
{
var args = ArgumentListParser.Parse(r, '[', ']', ';');
if (args.Count > 2 || args.Count == 0)
{
throw new ExpressionParseException(r.Position, "Too many arguments in RelativeSource syntax sugar");
}
else if (args.Count == 1)
{
if (int.TryParse(args[0], out int level))
{
ancestorType = null;
ancestorLevel = level;
}
else
{
var typeName = ParseTypeName(new CharacterReader(args[0]));
ancestorType = _typeResolver(typeName.ns, typeName.typeName);
}
}
else
{
var typeName = ParseTypeName(new CharacterReader(args[0]));
ancestorType = _typeResolver(typeName.ns, typeName.typeName);
ancestorLevel = int.Parse(args[1]);
}
}
nodes.Add(new FindAncestorNode(ancestorType, ancestorLevel));
}
else
{
throw new ExpressionParseException(r.Position, "Unknown RelativeSource mode.");
}
return State.AfterMember;
}
private static (string ns, string typeName) ParseTypeName(CharacterReader r)
{
string ns, typeName;
ns = string.Empty;
var typeNameOrNamespace = IdentifierParser.Parse(r);
if (!r.End && r.TakeIf(':'))
{
ns = typeNameOrNamespace;
typeName = IdentifierParser.Parse(r);
}
else
{
typeName = typeNameOrNamespace;
}
return (ns, typeName);
}
private static bool ParseNot(CharacterReader r)
{
return !r.End && r.TakeIf('!');
}
private static bool ParseMemberAccessor(Reader r)
private static bool ParseMemberAccessor(CharacterReader r)
{
return !r.End && r.TakeIf('.');
}
private static bool ParseOpenBrace(Reader r)
private static bool ParseOpenBrace(CharacterReader r)
{
return !r.End && r.TakeIf('(');
}
private static bool PeekOpenBracket(Reader r)
private static bool PeekOpenBracket(CharacterReader r)
{
return !r.End && r.Peek == '[';
}
private static bool ParseStreamOperator(Reader r)
private static bool ParseStreamOperator(CharacterReader r)
{
return !r.End && r.TakeIf('^');
}
private static bool ParseDollarSign(CharacterReader r)
{
return !r.End && r.TakeIf('$');
}
private static bool ParseSharp(CharacterReader r)
{
return !r.End && r.TakeIf('#');
}
private enum State
{
Start,
RelativeSource,
ElementName,
AfterMember,
BeforeMember,
AttachedProperty,

39
src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Data.Core;
using Avalonia.LogicalTree;
namespace Avalonia.Markup.Parsers.Nodes
{
internal class ElementNameNode : ExpressionNode
{
private readonly string _name;
private IDisposable _subscription;
public ElementNameNode(string name)
{
_name = name;
}
public override string Description => $"#{_name}";
protected override void StartListeningCore(WeakReference reference)
{
if (reference.Target is ILogical logical)
{
_subscription = ControlLocator.Track(logical, _name).Subscribe(ValueChanged);
}
else
{
_subscription = null;
}
}
protected override void StopListeningCore()
{
_subscription?.Dispose();
_subscription = null;
}
}
}

54
src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Data.Core;
using Avalonia.LogicalTree;
namespace Avalonia.Markup.Parsers.Nodes
{
internal class FindAncestorNode : ExpressionNode
{
private readonly int _level;
private readonly Type _ancestorType;
private IDisposable _subscription;
public FindAncestorNode(Type ancestorType, int level)
{
_level = level;
_ancestorType = ancestorType;
}
public override string Description
{
get
{
if (_ancestorType == null)
{
return $"$parent[{_level}]";
}
else
{
return $"$parent[{_ancestorType.Name}, {_level}]";
}
}
}
protected override void StartListeningCore(WeakReference reference)
{
if (reference.Target is ILogical logical)
{
_subscription = ControlLocator.Track(logical, _level, _ancestorType).Subscribe(ValueChanged);
}
else
{
_subscription = null;
}
}
protected override void StopListeningCore()
{
_subscription?.Dispose();
_subscription = null;
}
}
}

12
src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Data.Core;
namespace Avalonia.Markup.Parsers.Nodes
{
internal class SelfNode : ExpressionNode
{
public override string Description => "$self";
}
}

4
src/OSX/Avalonia.MonoMac/SystemDialogsImpl.cs

@ -25,9 +25,9 @@ namespace Avalonia.MonoMac
else
{
if (panel is NSOpenPanel openPanel)
tcs.SetResult(openPanel.Urls.Select(url => url.AbsoluteString).ToArray());
tcs.SetResult(openPanel.Urls.Select(url => url.AbsoluteString.Replace("file://", "")).ToArray());
else
tcs.SetResult(new[] { panel.Url.AbsoluteString });
tcs.SetResult(new[] { panel.Url.AbsoluteString.Replace("file://", "") });
}
panel.OrderOut(panel);
keyWindow?.MakeKeyAndOrderFront(keyWindow);

38
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -10,6 +10,7 @@ using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Utilities;
using Avalonia.Utilities;
using Avalonia.Visuals.Media.Imaging;
using SkiaSharp;
namespace Avalonia.Skia
@ -95,24 +96,46 @@ namespace Avalonia.Skia
}
/// <inheritdoc />
public void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
public void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
{
var drawableImage = (IDrawableBitmapImpl) source.Item;
var drawableImage = (IDrawableBitmapImpl)source.Item;
var s = sourceRect.ToSKRect();
var d = destRect.ToSKRect();
using (var paint =
new SKPaint {Color = new SKColor(255, 255, 255, (byte) (255 * opacity * _currentOpacity))})
new SKPaint
{
Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity))
})
{
paint.FilterQuality = GetInterpolationMode(bitmapInterpolationMode);
drawableImage.Draw(this, s, d, paint);
}
}
private static SKFilterQuality GetInterpolationMode(BitmapInterpolationMode interpolationMode)
{
switch (interpolationMode)
{
case BitmapInterpolationMode.LowQuality:
return SKFilterQuality.Low;
case BitmapInterpolationMode.MediumQuality:
return SKFilterQuality.Medium;
case BitmapInterpolationMode.HighQuality:
return SKFilterQuality.High;
case BitmapInterpolationMode.Default:
return SKFilterQuality.None;
default:
throw new ArgumentOutOfRangeException(nameof(interpolationMode), interpolationMode, null);
}
}
/// <inheritdoc />
public void DrawImage(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
{
PushOpacityMask(opacityMask, opacityMaskRect);
DrawImage(source, 1, new Rect(0, 0, source.Item.PixelWidth, source.Item.PixelHeight), destRect);
DrawImage(source, 1, new Rect(0, 0, source.Item.PixelWidth, source.Item.PixelHeight), destRect, BitmapInterpolationMode.Default);
PopOpacityMask();
}
@ -357,6 +380,7 @@ namespace Avalonia.Skia
/// <param name="targetSize">Target size.</param>
/// <param name="tileBrush">Tile brush to use.</param>
/// <param name="tileBrushImage">Tile brush image.</param>
/// <param name="interpolationMode">The bitmap interpolation mode.</param>
private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Size targetSize, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage)
{
var calc = new TileBrushCalculator(tileBrush,
@ -375,7 +399,7 @@ namespace Avalonia.Skia
context.Clear(Colors.Transparent);
context.PushClip(calc.IntermediateClip);
context.Transform = calc.IntermediateTransform;
context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect);
context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect, tileBrush.BitmapInterpolationMode);
context.PopClip();
}
@ -484,7 +508,7 @@ namespace Avalonia.Skia
}
else
{
tileBrushImage = (IDrawableBitmapImpl) (tileBrush as IImageBrush)?.Source?.PlatformImpl.Item;
tileBrushImage = (IDrawableBitmapImpl)(tileBrush as IImageBrush)?.Source?.PlatformImpl.Item;
}
if (tileBrush != null && tileBrushImage != null)
@ -690,4 +714,4 @@ namespace Avalonia.Skia
}
}
}
}
}

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

@ -10,13 +10,14 @@ using Avalonia.Utilities;
using SharpDX;
using SharpDX.Direct2D1;
using SharpDX.Mathematics.Interop;
using BitmapInterpolationMode = Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode;
namespace Avalonia.Direct2D1.Media
{
/// <summary>
/// Draws using Direct2D1.
/// </summary>
public class DrawingContextImpl : IDrawingContextImpl, IDisposable
public class DrawingContextImpl : IDrawingContextImpl
{
private readonly IVisualBrushRenderer _visualBrushRenderer;
private readonly ILayerFactory _layerFactory;
@ -100,19 +101,37 @@ namespace Avalonia.Direct2D1.Media
/// <param name="opacity">The opacity to draw with.</param>
/// <param name="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</param>
public void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
public void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
{
using (var d2d = ((BitmapImpl)source.Item).GetDirect2DBitmap(_renderTarget))
{
var interpolationMode = GetInterpolationMode(bitmapInterpolationMode);
_renderTarget.DrawBitmap(
d2d.Value,
destRect.ToSharpDX(),
(float)opacity,
BitmapInterpolationMode.Linear,
interpolationMode,
sourceRect.ToSharpDX());
}
}
private static SharpDX.Direct2D1.BitmapInterpolationMode GetInterpolationMode(BitmapInterpolationMode interpolationMode)
{
switch (interpolationMode)
{
case BitmapInterpolationMode.LowQuality:
return SharpDX.Direct2D1.BitmapInterpolationMode.NearestNeighbor;
case BitmapInterpolationMode.MediumQuality:
case BitmapInterpolationMode.HighQuality:
case BitmapInterpolationMode.Default:
return SharpDX.Direct2D1.BitmapInterpolationMode.Linear;
default:
throw new ArgumentOutOfRangeException(nameof(interpolationMode), interpolationMode, null);
}
}
/// <summary>
/// Draws a bitmap image.
/// </summary>

9
src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs

@ -1,7 +1,6 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Media;
using Avalonia.Rendering.Utilities;
using Avalonia.Utilities;
@ -11,7 +10,9 @@ namespace Avalonia.Direct2D1.Media
{
public sealed class ImageBrushImpl : BrushImpl
{
OptionalDispose<Bitmap> _bitmap;
private readonly OptionalDispose<Bitmap> _bitmap;
private readonly Visuals.Media.Imaging.BitmapInterpolationMode _bitmapInterpolationMode;
public ImageBrushImpl(
ITileBrush brush,
@ -41,6 +42,8 @@ namespace Avalonia.Direct2D1.Media
GetBrushProperties(brush, calc.DestinationRect));
}
}
_bitmapInterpolationMode = brush.BitmapInterpolationMode;
}
public override void Dispose()
@ -102,7 +105,7 @@ namespace Avalonia.Direct2D1.Media
context.PushClip(calc.IntermediateClip);
context.Transform = calc.IntermediateTransform;
context.DrawImage(RefCountable.CreateUnownedNotClonable(bitmap), 1, rect, rect);
context.DrawImage(RefCountable.CreateUnownedNotClonable(bitmap), 1, rect, rect, _bitmapInterpolationMode);
context.PopClip();
}

11
tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs

@ -99,17 +99,6 @@ namespace Avalonia.Base.UnitTests
Assert.Null(result);
}
[Fact]
public void FindRegisteredAttached_Finds_Property()
{
var result = AvaloniaPropertyRegistry.Instance.FindRegisteredAttached(
typeof(Class1),
typeof(AttachedOwner),
"Attached");
Assert.Equal(AttachedOwner.AttachedProperty, result);
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =

22
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
@ -13,6 +14,7 @@ using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Data;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests.Primitives
@ -686,6 +688,26 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Null(KeyboardNavigation.GetTabOnceActiveElement((InputElement)panel));
}
[Fact]
public void Resetting_Items_Collection_Should_Retain_Selection()
{
var itemsMock = new Mock<List<string>>();
var itemsMockAsINCC = itemsMock.As<INotifyCollectionChanged>();
itemsMock.Object.AddRange(new[] { "Foo", "Bar", "Baz" });
var target = new SelectingItemsControl
{
Items = itemsMock.Object
};
target.SelectedIndex = 1;
itemsMockAsINCC.Raise(e => e.CollectionChanged += null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
Assert.True(target.SelectedIndex == 1);
}
private FuncControlTemplate Template()
{
return new FuncControlTemplate<SelectingItemsControl>(control =>

39
tests/Avalonia.Controls.UnitTests/SliderTests.cs

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Text;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class SliderTests
{
[Fact]
public void Default_Orientation_Should_Be_Horizontal()
{
var slider = new Slider();
Assert.Equal(Orientation.Horizontal, slider.Orientation);
}
[Fact]
public void Should_Set_Horizontal_Class()
{
var slider = new Slider
{
Orientation = Orientation.Horizontal
};
Assert.Contains(slider.Classes, ":horizontal".Equals);
}
[Fact]
public void Should_Set_Vertical_Class()
{
var slider = new Slider
{
Orientation = Orientation.Vertical
};
Assert.Contains(slider.Classes, ":vertical".Equals);
}
}
}

14
tests/Avalonia.Controls.UnitTests/StackPanelTests.cs

@ -54,11 +54,11 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
public void Lays_Out_Children_Vertically_With_Gap()
public void Lays_Out_Children_Vertically_With_Spacing()
{
var target = new StackPanel
{
Gap = 10,
Spacing = 10,
Children =
{
new Border { Height = 20, Width = 120 },
@ -77,11 +77,11 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
public void Lays_Out_Children_Horizontally_With_Gap()
public void Lays_Out_Children_Horizontally_With_Spacing()
{
var target = new StackPanel
{
Gap = 10,
Spacing = 10,
Orientation = Orientation.Horizontal,
Children =
{
@ -150,11 +150,11 @@ namespace Avalonia.Controls.UnitTests
[Theory]
[InlineData(Orientation.Horizontal)]
[InlineData(Orientation.Vertical)]
public void Gap_Not_Added_For_Invisible_Children(Orientation orientation)
public void Spacing_Not_Added_For_Invisible_Children(Orientation orientation)
{
var targetThreeChildrenOneInvisble = new StackPanel
{
Gap = 40,
Spacing = 40,
Orientation = orientation,
Children =
{
@ -165,7 +165,7 @@ namespace Avalonia.Controls.UnitTests
};
var targetTwoChildrenNoneInvisible = new StackPanel
{
Gap = 40,
Spacing = 40,
Orientation = orientation,
Children =
{

3
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs

@ -162,8 +162,9 @@ namespace Avalonia.Markup.UnitTests.Parsers
Assert.Equal(e.Arguments.ToArray(), args);
}
private List<ExpressionNode> ToList(ExpressionNode node)
private List<ExpressionNode> ToList((ExpressionNode node, SourceMode mode) parsed)
{
var (node, _) = parsed;
var result = new List<ExpressionNode>();
while (node != null)

36
tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs

@ -10,6 +10,7 @@ using Xunit;
using System.ComponentModel;
using Portable.Xaml;
using Portable.Xaml.Markup;
using Avalonia.Controls;
namespace Avalonia.Markup.Xaml.UnitTests.Converters
{
@ -26,7 +27,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters
public void ConvertFrom_Finds_Fully_Qualified_Property()
{
var target = new AvaloniaPropertyTypeConverter();
var context = CreateContext();
var style = new Style(x => x.OfType<Class1>());
var context = CreateContext(style);
var result = target.ConvertFrom(context, null, "Class1.Foo");
Assert.Equal(Class1.FooProperty, result);
@ -47,7 +49,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters
public void ConvertFrom_Finds_Attached_Property()
{
var target = new AvaloniaPropertyTypeConverter();
var context = CreateContext();
var style = new Style(x => x.OfType<Class1>());
var context = CreateContext(style);
var result = target.ConvertFrom(context, null, "AttachedOwner.Attached");
Assert.Equal(AttachedOwner.AttachedProperty, result);
@ -57,12 +60,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters
public void ConvertFrom_Finds_Attached_Property_With_Parentheses()
{
var target = new AvaloniaPropertyTypeConverter();
var context = CreateContext();
var style = new Style(x => x.OfType<Class1>());
var context = CreateContext(style);
var result = target.ConvertFrom(context, null, "(AttachedOwner.Attached)");
Assert.Equal(AttachedOwner.AttachedProperty, result);
}
[Fact]
public void ConvertFrom_Throws_For_Nonexistent_Property()
{
var target = new AvaloniaPropertyTypeConverter();
var style = new Style(x => x.OfType<Class1>());
var context = CreateContext(style);
var ex = Assert.Throws<XamlLoadException>(() => target.ConvertFrom(context, null, "Nonexistent"));
Assert.Equal("Could not find property 'Class1.Nonexistent'.", ex.Message);
}
[Fact]
public void ConvertFrom_Throws_For_Nonexistent_Attached_Property()
{
var target = new AvaloniaPropertyTypeConverter();
var style = new Style(x => x.OfType<Class1>());
var context = CreateContext(style);
var ex = Assert.Throws<XamlLoadException>(() => target.ConvertFrom(context, null, "AttachedOwner.NonExistent"));
Assert.Equal("Could not find property 'AttachedOwner.NonExistent'.", ex.Message);
}
private ITypeDescriptorContext CreateContext(Style style = null)
{
var tdMock = new Mock<ITypeDescriptorContext>();
@ -126,4 +154,4 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters
AvaloniaProperty.RegisterAttached<AttachedOwner, Class1, string>("Attached");
}
}
}
}

226
tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs

@ -0,0 +1,226 @@
using System;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.Markup.Xaml.Parsers;
using Avalonia.Utilities;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Parsers
{
public class PropertyParserTests
{
[Fact]
public void Parses_Name()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo");
var (ns, owner, name) = target.Parse(reader);
Assert.Null(ns);
Assert.Null(owner);
Assert.Equal("Foo", name);
}
[Fact]
public void Parses_Owner_And_Name()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo.Bar");
var (ns, owner, name) = target.Parse(reader);
Assert.Null(ns);
Assert.Equal("Foo", owner);
Assert.Equal("Bar", name);
}
[Fact]
public void Parses_Namespace_Owner_And_Name()
{
var target = new PropertyParser();
var reader = new CharacterReader("foo:Bar.Baz");
var (ns, owner, name) = target.Parse(reader);
Assert.Equal("foo", ns);
Assert.Equal("Bar", owner);
Assert.Equal("Baz", name);
}
[Fact]
public void Parses_Owner_And_Name_With_Parentheses()
{
var target = new PropertyParser();
var reader = new CharacterReader("(Foo.Bar)");
var (ns, owner, name) = target.Parse(reader);
Assert.Null(ns);
Assert.Equal("Foo", owner);
Assert.Equal("Bar", name);
}
[Fact]
public void Parses_Namespace_Owner_And_Name_With_Parentheses()
{
var target = new PropertyParser();
var reader = new CharacterReader("(foo:Bar.Baz)");
var (ns, owner, name) = target.Parse(reader);
Assert.Equal("foo", ns);
Assert.Equal("Bar", owner);
Assert.Equal("Baz", name);
}
[Fact]
public void Fails_With_Empty_String()
{
var target = new PropertyParser();
var reader = new CharacterReader("");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(0, ex.Column);
Assert.Equal("Expected property name.", ex.Message);
}
[Fact]
public void Fails_With_Only_Whitespace()
{
var target = new PropertyParser();
var reader = new CharacterReader(" ");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(0, ex.Column);
Assert.Equal("Unexpected ' '.", ex.Message);
}
[Fact]
public void Fails_With_Leading_Whitespace()
{
var target = new PropertyParser();
var reader = new CharacterReader(" Foo");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(0, ex.Column);
Assert.Equal("Unexpected ' '.", ex.Message);
}
[Fact]
public void Fails_With_Trailing_Whitespace()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo ");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(3, ex.Column);
Assert.Equal("Unexpected ' '.", ex.Message);
}
[Fact]
public void Fails_With_Invalid_Property_Name()
{
var target = new PropertyParser();
var reader = new CharacterReader("123");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(0, ex.Column);
Assert.Equal("Unexpected '1'.", ex.Message);
}
[Fact]
public void Fails_With_Trailing_Junk()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo%");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(3, ex.Column);
Assert.Equal("Unexpected '%'.", ex.Message);
}
[Fact]
public void Fails_With_Invalid_Property_Name_After_Owner()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo.123");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(4, ex.Column);
Assert.Equal("Unexpected '1'.", ex.Message);
}
[Fact]
public void Fails_With_Whitespace_Between_Owner_And_Name()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo. Bar");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(4, ex.Column);
Assert.Equal("Unexpected ' '.", ex.Message);
}
[Fact]
public void Fails_With_Too_Many_Segments()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo.Bar.Baz");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(8, ex.Column);
Assert.Equal("Unexpected '.'.", ex.Message);
}
[Fact]
public void Fails_With_Too_Many_Namespaces()
{
var target = new PropertyParser();
var reader = new CharacterReader("foo:bar:Baz");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(8, ex.Column);
Assert.Equal("Unexpected ':'.", ex.Message);
}
[Fact]
public void Fails_With_Parens_But_No_Owner()
{
var target = new PropertyParser();
var reader = new CharacterReader("(Foo)");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(1, ex.Column);
Assert.Equal("Expected property owner.", ex.Message);
}
[Fact]
public void Fails_With_Parens_And_Namespace_But_No_Owner()
{
var target = new PropertyParser();
var reader = new CharacterReader("(foo:Bar)");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(1, ex.Column);
Assert.Equal("Expected property owner.", ex.Message);
}
[Fact]
public void Fails_With_Missing_Close_Parens()
{
var target = new PropertyParser();
var reader = new CharacterReader("(Foo.Bar");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(8, ex.Column);
Assert.Equal("Expected ')'.", ex.Message);
}
[Fact]
public void Fails_With_Unexpected_Close_Parens()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo.Bar)");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(7, ex.Column);
Assert.Equal("Unexpected ')'.", ex.Message);
}
}
}

29
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs

@ -281,5 +281,32 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal(5.6, AttachedPropertyOwner.GetDouble(textBlock));
}
}
[Fact]
public void Binding_To_Attached_Property_In_Style_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Window.Styles>
<Style Selector='TextBlock'>
<Setter Property='local:TestControl.Double' Value='{Binding}'/>
</Style>
</Window.Styles>
<TextBlock/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = (TextBlock)window.Content;
window.DataContext = 5.6;
window.ApplyTemplate();
Assert.Equal(5.6, AttachedPropertyOwner.GetDouble(textBlock));
}
}
}
}
}

55
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs

@ -297,7 +297,60 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal("title", button.Content);
}
}
[Fact]
public void Shorthand_Binding_With_Negation_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Border Name='border1'>
<Border Name='border2'>
<Button Name='button' Content='{Binding !$self.IsDefault}'/>
</Border>
</Border>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
#pragma warning disable xUnit2004 // Diagnostic mis-firing since button.Content isn't guaranteed to be a bool.
Assert.Equal(true, button.Content);
#pragma warning restore xUnit2004
}
}
[Fact]
public void Shorthand_Binding_With_Multiple_Negation_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Border Name='border1'>
<Border Name='border2'>
<Button Name='button' Content='{Binding !!$self.IsDefault}'/>
</Border>
</Border>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
#pragma warning disable xUnit2004 // Diagnostic mis-firing since button.Content isn't guaranteed to be a bool.
Assert.Equal(false, button.Content);
#pragma warning restore xUnit2004
}
}
}
public class TestWindow : Window { }
}
}

54
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@ -7,6 +7,7 @@ using Avalonia.Markup.Xaml.Styling;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Portable.Xaml;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
@ -146,5 +147,56 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.NotNull(target.FocusAdorner);
}
}
[Fact]
public void Setter_Can_Set_Attached_Property()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Window.Styles>
<Style Selector='TextBlock'>
<Setter Property='DockPanel.Dock' Value='Right'/>
</Style>
</Window.Styles>
<TextBlock/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = (TextBlock)window.Content;
window.ApplyTemplate();
Assert.Equal(Dock.Right, DockPanel.GetDock(textBlock));
}
}
[Fact(Skip = "The animation system currently needs to be able to set any property on any object")]
public void Disallows_Setting_Non_Registered_Property()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Window.Styles>
<Style Selector='TextBlock'>
<Setter Property='Button.IsDefault' Value='True'/>
</Style>
</Window.Styles>
<TextBlock/>
</Window>";
var loader = new AvaloniaXamlLoader();
var ex = Assert.Throws<XamlObjectWriterException>(() => loader.Load(xaml));
Assert.Equal(
"Property 'Button.IsDefault' is not registered on 'Avalonia.Controls.TextBlock'.",
ex.InnerException.Message);
}
}
}
}
}

6
tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Media;
@ -10,8 +11,11 @@ using Avalonia.Rendering;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Avalonia.Visuals.Media.Imaging;
using Avalonia.VisualTree;
using Moq;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Rendering
@ -336,7 +340,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
var context = Mock.Get(target.RenderTarget.CreateDrawingContext(null));
var borderLayer = target.Layers[border].Bitmap;
context.Verify(x => x.DrawImage(borderLayer, 0.5, It.IsAny<Rect>(), It.IsAny<Rect>()));
context.Verify(x => x.DrawImage(borderLayer, 0.5, It.IsAny<Rect>(), It.IsAny<Rect>(), BitmapInterpolationMode.Default));
}
private DeferredRenderer CreateTargetAndRunFrame(

14
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs

@ -1,13 +1,13 @@
using System;
using Avalonia.Media;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities;
using Avalonia.Visuals.Media.Imaging;
using Moq;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
{
{
public class DrawOperationTests
{
[Fact]
@ -46,7 +46,13 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
public void Image_Node_Releases_Reference_To_Bitmap_On_Dispose()
{
var bitmap = RefCountable.Create(Mock.Of<IBitmapImpl>());
var imageNode = new ImageNode(Matrix.Identity, bitmap, 1, new Rect(1,1,1,1), new Rect(1,1,1,1));
var imageNode = new ImageNode(
Matrix.Identity,
bitmap,
1,
new Rect(1, 1, 1, 1),
new Rect(1, 1, 1, 1),
BitmapInterpolationMode.Default);
Assert.Equal(2, bitmap.RefCount);

Loading…
Cancel
Save