Browse Source

Merge remote-tracking branch 'AvaloniaUI/master' into selector-parse-no-sprache

pull/1668/head
Jeremy Koritzinsky 8 years ago
parent
commit
547d4dc906
  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. 126
      src/Avalonia.Animation/Animation.cs
  28. 66
      src/Avalonia.Animation/AnimatorKeyFrame.cs
  29. 191
      src/Avalonia.Animation/AnimatorStateMachine`1.cs
  30. 80
      src/Avalonia.Animation/Animator`1.cs
  31. 2
      src/Avalonia.Animation/Cue.cs
  32. 8
      src/Avalonia.Animation/DoubleAnimator.cs
  33. 10
      src/Avalonia.Animation/IAnimation.cs
  34. 2
      src/Avalonia.Animation/IAnimationSetter.cs
  35. 2
      src/Avalonia.Animation/IAnimator.cs
  36. 16
      src/Avalonia.Animation/KeyFrame.cs
  37. 8
      src/Avalonia.Animation/KeyFramePair`1.cs
  38. 3
      src/Avalonia.Base/Avalonia.Base.csproj
  39. 51
      src/Avalonia.Base/AvaloniaObject.cs
  40. 52
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  41. 49
      src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs
  42. 25
      src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs
  43. 3
      src/Avalonia.Base/IPriorityValueOwner.cs
  44. 11
      src/Avalonia.Base/PriorityValue.cs
  45. 6
      src/Avalonia.Base/Utilities/CharacterReader.cs
  46. 53
      src/Avalonia.Base/Utilities/DeferredSetter.cs
  47. 6
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  48. 58
      src/Avalonia.Base/Utilities/SingleOrQueue.cs
  49. 24
      src/Avalonia.Base/ValueStore.cs
  50. 71
      src/Avalonia.Controls/ContextMenu.cs
  51. 6
      src/Avalonia.Controls/Expander.cs
  52. 20
      src/Avalonia.Controls/Grid.cs
  53. 9
      src/Avalonia.Controls/Image.cs
  54. 5
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  55. 36
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  56. 30
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  57. 6
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  58. 25
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  59. 9
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  60. 2
      src/Avalonia.Controls/Primitives/ToggleButton.cs
  61. 90
      src/Avalonia.Controls/ProgressBar.cs
  62. 5
      src/Avalonia.Controls/Slider.cs
  63. 41
      src/Avalonia.Controls/StackPanel.cs
  64. 16
      src/Avalonia.Controls/TextBox.cs
  65. 12
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  66. 10
      src/Avalonia.Controls/Window.cs
  67. 4
      src/Avalonia.Controls/WindowCollection.cs
  68. 12
      src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj
  69. 4
      src/Avalonia.Diagnostics/DevTools.xaml
  70. 1
      src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
  71. 4
      src/Avalonia.Diagnostics/Views/TreePageView.xaml
  72. 1
      src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj
  73. 17
      src/Avalonia.Styling/Styling/Setter.cs
  74. 5
      src/Avalonia.Themes.Default/MenuItem.xaml
  75. 45
      src/Avalonia.Themes.Default/ProgressBar.xaml
  76. 60
      src/Avalonia.Visuals/Animation/CrossFade.cs
  77. 2
      src/Avalonia.Visuals/Animation/IPageTransition.cs
  78. 64
      src/Avalonia.Visuals/Animation/PageSlide.cs
  79. 6
      src/Avalonia.Visuals/Animation/TransformAnimator.cs
  80. 10
      src/Avalonia.Visuals/Media/DrawingContext.cs
  81. 19
      src/Avalonia.Visuals/Media/ITileBrush.cs
  82. 31
      src/Avalonia.Visuals/Media/Imaging/BitmapInterpolationMode.cs
  83. 11
      src/Avalonia.Visuals/Media/Immutable/ImmutableImageBrush.cs
  84. 16
      src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs
  85. 12
      src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs
  86. 39
      src/Avalonia.Visuals/Media/RenderOptions.cs
  87. 19
      src/Avalonia.Visuals/Media/TileBrush.cs
  88. 4
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  89. 7
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  90. 23
      src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs
  91. 7
      src/Gtk/Avalonia.Gtk3/KeyTransform.cs
  92. 1
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  93. 68
      src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs
  94. 165
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  95. 85
      src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs
  96. 4
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaRuntimeTypeProvider.cs
  97. 2
      src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
  98. 119
      src/Markup/Avalonia.Markup/Data/Binding.cs
  99. 6
      src/Markup/Avalonia.Markup/Data/TemplateBinding.cs
  100. 11
      src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs

5
.gitignore

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

26
samples/BindingDemo/MainWindow.xaml

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

6
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml

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

6
samples/ControlCatalog/Pages/BorderPage.xaml

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

10
samples/ControlCatalog/Pages/ButtonPage.xaml

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

6
samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml

@ -1,11 +1,11 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">ButtonSpinner</TextBlock> <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> <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="allowSpinCheck" IsChecked="True">AllowSpin</CheckBox>
<CheckBox Name="showSpinCheck" IsChecked="True">ShowButtonSpinner</CheckBox> <CheckBox Name="showSpinCheck" IsChecked="True">ShowButtonSpinner</CheckBox>
<ButtonSpinner Spin="OnSpin" Height="30" <ButtonSpinner Spin="OnSpin" Height="30"
@ -21,4 +21,4 @@
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

6
samples/ControlCatalog/Pages/CalendarPage.xaml

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

4
samples/ControlCatalog/Pages/CanvasPage.xaml

@ -1,5 +1,5 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Canvas</TextBlock> <TextBlock Classes="h1">Canvas</TextBlock>
<TextBlock Classes="h2">A panel which lays out its children by explicit coordinates</TextBlock> <TextBlock Classes="h2">A panel which lays out its children by explicit coordinates</TextBlock>
<Canvas Background="Yellow" Width="300" Height="400"> <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"/> <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> </Canvas>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

10
samples/ControlCatalog/Pages/CarouselPage.xaml

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

10
samples/ControlCatalog/Pages/CheckBoxPage.xaml

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

6
samples/ControlCatalog/Pages/ContextMenuPage.xaml

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

6
samples/ControlCatalog/Pages/DatePickerPage.xaml

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

4
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -1,5 +1,5 @@
<UserControl xmlns="https://github.com/avaloniaui"> <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="OpenFile">Open File</Button>
<Button Name="SaveFile">Save File</Button> <Button Name="SaveFile">Save File</Button>
<Button Name="SelectFolder">Select Folder</Button> <Button Name="SelectFolder">Select Folder</Button>
@ -9,4 +9,4 @@
</StackPanel> </StackPanel>
<Button Name="DecoratedWindow">Decorated window</Button> <Button Name="DecoratedWindow">Decorated window</Button>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

6
samples/ControlCatalog/Pages/DragAndDropPage.xaml

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

6
samples/ControlCatalog/Pages/DropDownPage.xaml

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

6
samples/ControlCatalog/Pages/ExpanderPage.xaml

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

4
samples/ControlCatalog/Pages/ImagePage.xaml

@ -1,12 +1,12 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Image</TextBlock> <TextBlock Classes="h1">Image</TextBlock>
<TextBlock Classes="h2">Displays an image</TextBlock> <TextBlock Classes="h2">Displays an image</TextBlock>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<TextBlock>No Stretch</TextBlock> <TextBlock>No Stretch</TextBlock>
<Image Source="resm:ControlCatalog.Assets.delicate-arch-896885_640.jpg" <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"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Menu</TextBlock> <TextBlock Classes="h1">Menu</TextBlock>
<TextBlock Classes="h2">A window menu</TextBlock> <TextBlock Classes="h2">A window menu</TextBlock>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<Menu> <Menu>
<MenuItem Header="_First"> <MenuItem Header="_First">
<MenuItem Header="Standard _Menu Item"/> <MenuItem Header="Standard _Menu Item"/>

6
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@ -1,6 +1,6 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Margin="2" Classes="h1">Numeric up-down control</TextBlock> <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> <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"> VerticalAlignment="Center" Margin="2">
<DropDown.ItemTemplate> <DropDown.ItemTemplate>
<DataTemplate> <DataTemplate>
<StackPanel Orientation="Horizontal" Gap="2"> <StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="{Binding Name}"/> <TextBlock Text="{Binding Name}"/>
<TextBlock Text="-"/> <TextBlock Text="-"/>
<TextBlock Text="{Binding Value}"/> <TextBlock Text="{Binding Value}"/>
@ -69,7 +69,7 @@
</Grid> </Grid>
</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> <TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of NumericUpDown:</TextBlock>
<NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5" <NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5"
CultureInfo="en-US" VerticalAlignment="Center" Height="25" Width="100" 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"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">ProgressBar</TextBlock> <TextBlock Classes="h1">ProgressBar</TextBlock>
<TextBlock Classes="h2">A progress bar control</TextBlock> <TextBlock Classes="h2">A progress bar control</TextBlock>
@ -7,8 +7,8 @@
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<StackPanel Gap="16"> <StackPanel Spacing="16">
<ProgressBar Value="{Binding #hprogress.Value}" /> <ProgressBar Value="{Binding #hprogress.Value}" />
<ProgressBar IsIndeterminate="True"/> <ProgressBar IsIndeterminate="True"/>
</StackPanel> </StackPanel>

8
samples/ControlCatalog/Pages/RadioButtonPage.xaml

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

11
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -1,14 +1,15 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">TextBox</TextBlock> <TextBlock Classes="h1">TextBox</TextBlock>
<TextBlock Classes="h2">A control into which the user can input text</TextBlock> <TextBlock Classes="h2">A control into which the user can input text</TextBlock>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<StackPanel Orientation="Vertical" Gap="8"> <StackPanel Orientation="Vertical" Spacing="8">
<TextBox Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit." Width="200" /> <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="Watermark" />
<TextBox Width="200" <TextBox Width="200"
Watermark="Floating Watermark" Watermark="Floating Watermark"
@ -25,13 +26,13 @@
<TextBox Width="200" Text="Right aligned text" TextAlignment="Right" /> <TextBox Width="200" Text="Right aligned text" TextAlignment="Right" />
</StackPanel> </StackPanel>
<StackPanel Orientation="Vertical" Gap="8"> <StackPanel Orientation="Vertical" Spacing="8">
<TextBox AcceptsReturn="True" TextWrapping="Wrap" Width="200" Height="125" <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." /> 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" <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." /> 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>
<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 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 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"/> <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"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" <StackPanel Orientation="Vertical"
Gap="4"> Spacing="4">
<TextBlock Classes="h1">ToolTip</TextBlock> <TextBlock Classes="h1">ToolTip</TextBlock>
<TextBlock Classes="h2">A control which pops up a hint when a control is hovered</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"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">TreeView</TextBlock> <TextBlock Classes="h1">TreeView</TextBlock>
<TextBlock Classes="h2">Displays a hierachical tree of data.</TextBlock> <TextBlock Classes="h2">Displays a hierachical tree of data.</TextBlock>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<TreeView Items="{Binding}" Width="250" Height="350"> <TreeView Items="{Binding}" Width="250" Height="350">
<TreeView.ItemTemplate> <TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}"> <TreeDataTemplate ItemsSource="{Binding Children}">

2
samples/VirtualizationDemo/MainWindow.xaml

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

126
src/Avalonia.Animation/Animation.cs

@ -10,13 +10,16 @@ using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Reflection; using System.Reflection;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using System.Reactive.Linq;
using System.Reactive.Disposables;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
/// <summary> /// <summary>
/// Tracks the progress of an animation. /// Tracks the progress of an animation.
/// </summary> /// </summary>
public class Animation : AvaloniaList<KeyFrame>, IDisposable, IAnimation public class Animation : AvaloniaList<KeyFrame>, IAnimation
{ {
private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator)> Animators = new List<(Func<AvaloniaProperty, bool>, Type)> private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator)> Animators = new List<(Func<AvaloniaProperty, bool>, Type)>
{ {
@ -24,7 +27,7 @@ namespace Avalonia.Animation
}; };
public static void RegisterAnimator<TAnimator>(Func<AvaloniaProperty, bool> condition) public static void RegisterAnimator<TAnimator>(Func<AvaloniaProperty, bool> condition)
where TAnimator: IAnimator where TAnimator : IAnimator
{ {
Animators.Insert(0, (condition, typeof(TAnimator))); Animators.Insert(0, (condition, typeof(TAnimator)));
} }
@ -41,8 +44,6 @@ namespace Avalonia.Animation
return null; return null;
} }
private bool _isChildrenChanged = false;
private List<IDisposable> _subscription = new List<IDisposable>();
public AvaloniaList<IAnimator> _animators { get; set; } = new AvaloniaList<IAnimator>(); public AvaloniaList<IAnimator> _animators { get; set; } = new AvaloniaList<IAnimator>();
/// <summary> /// <summary>
@ -68,22 +69,18 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// The value fill mode for this animation. /// The value fill mode for this animation.
/// </summary> /// </summary>
public FillMode FillMode { get; set; } public FillMode FillMode { get; set; }
/// <summary> /// <summary>
/// Easing function to be used. /// Easing function to be used.
/// </summary> /// </summary>
public Easing Easing { get; set; } = new LinearEasing(); public Easing Easing { get; set; } = new LinearEasing();
public Animation() private (IList<IAnimator> Animators, IList<IDisposable> subscriptions) InterpretKeyframes(Animatable control)
{
this.CollectionChanged += delegate { _isChildrenChanged = true; };
}
private void InterpretKeyframes()
{ {
var handlerList = new List<(Type, AvaloniaProperty)>(); var handlerList = new List<(Type type, AvaloniaProperty property)>();
var kfList = new List<AnimatorKeyFrame>(); var animatorKeyFrames = new List<AnimatorKeyFrame>();
var subscriptions = new List<IDisposable>();
foreach (var keyframe in this) foreach (var keyframe in this)
{ {
@ -99,68 +96,87 @@ namespace Avalonia.Animation
if (!handlerList.Contains((handler, setter.Property))) if (!handlerList.Contains((handler, setter.Property)))
handlerList.Add((handler, setter.Property)); handlerList.Add((handler, setter.Property));
var newKF = new AnimatorKeyFrame() var cue = keyframe.Cue;
if (keyframe.TimingMode == KeyFrameTimingMode.TimeSpan)
{ {
Handler = handler, cue = new Cue(keyframe.KeyTime.Ticks / Duration.Ticks);
Property = setter.Property, }
Cue = keyframe.Cue,
KeyTime = keyframe.KeyTime, var newKF = new AnimatorKeyFrame(handler, cue);
timeSpanSet = keyframe.timeSpanSet,
cueSet = keyframe.cueSet, subscriptions.Add(newKF.BindSetter(setter, control));
Value = setter.Value
}; animatorKeyFrames.Add(newKF);
kfList.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); var newInstance = (IAnimator)Activator.CreateInstance(handlerType);
newInstance.Property = handler.Item2; newInstance.Property = property;
newAnimatorInstances.Add((handler.Item1, handler.Item2, newInstance)); newAnimatorInstances.Add(newInstance);
} }
foreach (var kf in kfList) foreach (var keyframe in animatorKeyFrames)
{ {
var parent = newAnimatorInstances.Where(p => p.handler == kf.Handler && var animator = newAnimatorInstances.First(a => a.GetType() == keyframe.AnimatorType &&
p.prop == kf.Property) a.Property == keyframe.Property);
.First(); animator.Add(keyframe);
parent.inst.Add(kf);
} }
foreach(var instance in newAnimatorInstances) return (newAnimatorInstances, subscriptions);
_animators.Add(instance.inst);
} }
/// <summary> /// <inheritdocs/>
/// Cancels the animation. public IDisposable Apply(Animatable control, IObservable<bool> match, Action onComplete)
/// </summary>
public void Dispose()
{ {
foreach (var sub in _subscription) var (animators, subscriptions) = InterpretKeyframes(control);
if (animators.Count == 1)
{
subscriptions.Add(animators[0].Apply(this, control, match, onComplete));
}
else
{ {
sub.Dispose(); var completionTasks = onComplete != null ? new List<Task>() : null;
foreach (IAnimator animator in animators)
{
Action animatorOnComplete = null;
if (onComplete != null)
{
var tcs = new TaskCompletionSource<object>();
animatorOnComplete = () => tcs.SetResult(null);
completionTasks.Add(tcs.Task);
}
subscriptions.Add(animator.Apply(this, control, match, animatorOnComplete));
}
if (onComplete != null)
{
Task.WhenAll(completionTasks).ContinueWith(_ => onComplete());
}
} }
return new CompositeDisposable(subscriptions);
} }
/// <inheritdocs/> /// <inheritdocs/>
public IDisposable Apply(Animatable control, IObservable<bool> matchObs) public Task RunAsync(Animatable control)
{ {
if (_isChildrenChanged) var run = new TaskCompletionSource<object>();
{
InterpretKeyframes();
_isChildrenChanged = false;
}
foreach (IAnimator keyframes in _animators) if (this.RepeatCount == RepeatCount.Loop)
run.SetException(new InvalidOperationException("Looping animations must not use the Run method."));
IDisposable subscriptions = null;
subscriptions = this.Apply(control, Observable.Return(true), () =>
{ {
_subscription.Add(keyframes.Apply(this, control, matchObs)); run.SetResult(null);
} subscriptions?.Dispose();
return this; });
return run.Task;
} }
} }
} }

66
src/Avalonia.Animation/AnimatorKeyFrame.cs

@ -4,6 +4,8 @@ using System.Text;
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Reactive;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
@ -11,13 +13,63 @@ namespace Avalonia.Animation
/// Defines a KeyFrame that is used for /// Defines a KeyFrame that is used for
/// <see cref="Animator{T}"/> objects. /// <see cref="Animator{T}"/> objects.
/// </summary> /// </summary>
public class AnimatorKeyFrame public class AnimatorKeyFrame : AvaloniaObject
{ {
public Type Handler; public static readonly DirectProperty<AnimatorKeyFrame, object> ValueProperty =
public Cue Cue; AvaloniaProperty.RegisterDirect<AnimatorKeyFrame, object>(nameof(Value), k => k.Value, (k, v) => k.Value = v);
public TimeSpan KeyTime;
internal bool timeSpanSet, cueSet; public AnimatorKeyFrame()
public AvaloniaProperty Property; {
public object Value;
}
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));
}
} }
} }

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

@ -35,6 +35,7 @@ namespace Avalonia.Animation
private T _neutralValue; private T _neutralValue;
internal bool _unsubscribe = false; internal bool _unsubscribe = false;
private IObserver<object> _targetObserver; private IObserver<object> _targetObserver;
private readonly Action _onComplete;
[Flags] [Flags]
private enum KeyFramesStates private enum KeyFramesStates
@ -51,9 +52,9 @@ namespace Avalonia.Animation
Disposed Disposed
} }
public void Initialize(Animation animation, Animatable control, Animator<T> keyframes) public AnimatorStateMachine(Animation animation, Animatable control, Animator<T> animator, Action onComplete)
{ {
_parent = keyframes; _parent = animator;
_targetAnimation = animation; _targetAnimation = animation;
_targetControl = control; _targetControl = control;
_neutralValue = (T)_targetControl.GetValue(_parent.Property); _neutralValue = (T)_targetControl.GetValue(_parent.Property);
@ -82,6 +83,7 @@ namespace Avalonia.Animation
_currentState = KeyFramesStates.DoDelay; _currentState = KeyFramesStates.DoDelay;
else else
_currentState = KeyFramesStates.DoRun; _currentState = KeyFramesStates.DoRun;
_onComplete = onComplete;
} }
public void Step(PlayState _playState, Func<double, T, T> Interpolator) public void Step(PlayState _playState, Func<double, T, T> Interpolator)
@ -123,121 +125,136 @@ namespace Avalonia.Animation
double _tempDuration = 0d, _easedTime; double _tempDuration = 0d, _easedTime;
checkstate: bool handled = false;
switch (_currentState)
while (!handled)
{ {
case KeyFramesStates.DoDelay: switch (_currentState)
{
case KeyFramesStates.DoDelay:
if (_fillMode == FillMode.Backward if (_fillMode == FillMode.Backward
|| _fillMode == FillMode.Both) || _fillMode == FillMode.Both)
{
if (_currentIteration == 0)
{ {
_targetObserver.OnNext(_firstKFValue); if (_currentIteration == 0)
{
_targetObserver.OnNext(_firstKFValue);
}
else
{
_targetObserver.OnNext(_lastInterpValue);
}
}
if (_delayFrameCount > _delayTotalFrameCount)
{
_currentState = KeyFramesStates.DoRun;
} }
else else
{ {
_targetObserver.OnNext(_lastInterpValue); handled = true;
_delayFrameCount++;
} }
} break;
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;
}
_tempDuration = (double)_durationFrameCount / _durationTotalFrameCount; case KeyFramesStates.DoRun:
_currentState = KeyFramesStates.RunApplyValue;
goto checkstate; if (_isReversed)
_currentState = KeyFramesStates.RunBackwards;
else
_currentState = KeyFramesStates.RunForwards;
case KeyFramesStates.RunBackwards: break;
if (_durationFrameCount > _durationTotalFrameCount) case KeyFramesStates.RunForwards:
{
_currentState = KeyFramesStates.RunComplete;
goto checkstate;
}
_tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount; if (_durationFrameCount > _durationTotalFrameCount)
_currentState = KeyFramesStates.RunApplyValue; {
_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++; case KeyFramesStates.RunApplyValue:
_lastInterpValue = Interpolator(_easedTime, _neutralValue);
_targetObserver.OnNext(_lastInterpValue);
_currentState = KeyFramesStates.DoRun;
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) case KeyFramesStates.RunComplete:
{
_delayFrameCount = 0;
_durationFrameCount = 0;
if (_isLooping) if (_checkLoopAndRepeat)
{
_currentState = KeyFramesStates.DoRun;
}
else if (_isRepeating)
{ {
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 if (_animationDirection == PlaybackDirection.Alternate
|| _animationDirection == PlaybackDirection.AlternateReverse) || _animationDirection == PlaybackDirection.AlternateReverse)
_isReversed = !_isReversed; _isReversed = !_isReversed;
break;
}
_currentState = KeyFramesStates.Stop;
break; break;
}
_currentState = KeyFramesStates.Stop; case KeyFramesStates.Stop:
goto checkstate;
case KeyFramesStates.Stop: if (_fillMode == FillMode.Forward
|| _fillMode == FillMode.Both)
{
_targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue);
}
if (_fillMode == FillMode.Forward _targetObserver.OnCompleted();
|| _fillMode == FillMode.Both) _onComplete?.Invoke();
{ Dispose();
_targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue); handled = true;
} break;
_targetObserver.OnCompleted(); default:
break; handled = true;
break;
}
} }
} }
@ -253,4 +270,4 @@ namespace Avalonia.Animation
_currentState = KeyFramesStates.Disposed; _currentState = KeyFramesStates.Disposed;
} }
} }
} }

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

@ -19,7 +19,7 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// List of type-converted keyframes. /// List of type-converted keyframes.
/// </summary> /// </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; private bool _isVerfifiedAndConverted;
@ -35,18 +35,17 @@ namespace Avalonia.Animation
} }
/// <inheritdoc/> /// <inheritdoc/>
public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch) public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch, Action onComplete)
{ {
if (!_isVerfifiedAndConverted) if (!_isVerfifiedAndConverted)
VerifyConvertKeyFrames(animation, typeof(T)); VerifyConvertKeyFrames();
return obsMatch return obsMatch
.Where(p => p == true)
// Ignore triggers when global timers are paused. // Ignore triggers when global timers are paused.
.Where(p => Timing.GetGlobalPlayState() != PlayState.Pause) .Where(p => p && Timing.GetGlobalPlayState() != PlayState.Pause)
.Subscribe(_ => .Subscribe(_ =>
{ {
var timerObs = RunKeyFrames(animation, control); var timerObs = RunKeyFrames(animation, control, onComplete);
}); });
} }
@ -60,8 +59,8 @@ namespace Avalonia.Animation
/// <param name="t">The time parameter, relative to the total animation time</param> /// <param name="t">The time parameter, relative to the total animation time</param>
protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t) protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t)
{ {
KeyValuePair<double, (T, bool)> firstCue, lastCue; KeyValuePair<double, (AnimatorKeyFrame frame, bool isNeutral)> firstCue, lastCue;
int kvCount = _convertedKeyframes.Count(); int kvCount = _convertedKeyframes.Count;
if (kvCount > 2) if (kvCount > 2)
{ {
if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0) if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0)
@ -76,8 +75,8 @@ namespace Avalonia.Animation
} }
else else
{ {
firstCue = _convertedKeyframes.Where(j => j.Key <= t).Last(); firstCue = _convertedKeyframes.Last(j => j.Key <= t);
lastCue = _convertedKeyframes.Where(j => j.Key >= t).First(); lastCue = _convertedKeyframes.First(j => j.Key >= t);
} }
} }
else else
@ -89,26 +88,24 @@ namespace Avalonia.Animation
double t0 = firstCue.Key; double t0 = firstCue.Key;
double t1 = lastCue.Key; double t1 = lastCue.Key;
var intraframeTime = (t - t0) / (t1 - t0); 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));
} }
/// <summary> /// <summary>
/// Runs the KeyFrames Animation. /// Runs the KeyFrames Animation.
/// </summary> /// </summary>
private IDisposable RunKeyFrames(Animation animation, Animatable control) private IDisposable RunKeyFrames(Animation animation, Animatable control, Action onComplete)
{ {
var _kfStateMach = new AnimatorStateMachine<T>(); var stateMachine = new AnimatorStateMachine<T>(animation, control, this, onComplete);
_kfStateMach.Initialize(animation, control, this);
Timing.AnimationStateTimer Timing.AnimationStateTimer
.TakeWhile(_ => !_kfStateMach._unsubscribe) .TakeWhile(_ => !stateMachine._unsubscribe)
.Subscribe(p => .Subscribe(p => stateMachine.Step(p, DoInterpolation));
{
_kfStateMach.Step(p, DoInterpolation);
});
return control.Bind(Property, _kfStateMach, BindingPriority.Animation); return control.Bind(Property, stateMachine, BindingPriority.Animation);
} }
/// <summary> /// <summary>
@ -119,39 +116,19 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Verifies and converts keyframe values according to this class's target type. /// Verifies and converts keyframe values according to this class's target type.
/// </summary> /// </summary>
private void VerifyConvertKeyFrames(Animation animation, Type type) private void VerifyConvertKeyFrames()
{ {
var typeConv = TypeDescriptor.GetConverter(type); foreach (AnimatorKeyFrame keyframe in this)
foreach (AnimatorKeyFrame k in this)
{ {
if (k.Value == null) _convertedKeyframes.Add(keyframe.Cue.CueValue, (keyframe, false));
{
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));
} }
SortKeyFrameCues(_convertedKeyframes); AddNeutralKeyFramesIfNeeded();
_isVerfifiedAndConverted = true; _isVerfifiedAndConverted = true;
} }
private void SortKeyFrameCues(Dictionary<double, (T, bool)> convertedValues) private void AddNeutralKeyFramesIfNeeded()
{ {
bool hasStartKey, hasEndKey; bool hasStartKey, hasEndKey;
hasStartKey = hasEndKey = false; hasStartKey = hasEndKey = false;
@ -170,23 +147,20 @@ namespace Avalonia.Animation
} }
if (!hasStartKey || !hasEndKey) if (!hasStartKey || !hasEndKey)
AddNeutralKeyFrames(hasStartKey, hasEndKey, _convertedKeyframes); AddNeutralKeyFrames(hasStartKey, hasEndKey);
_convertedKeyframes = _convertedKeyframes.OrderBy(p => p.Key)
.ToDictionary((k) => k.Key, (v) => v.Value);
} }
private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey, Dictionary<double, (T, bool)> convertedKeyframes) private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey)
{ {
if (!hasStartKey) if (!hasStartKey)
{ {
convertedKeyframes.Add(0.0d, (default(T), true)); _convertedKeyframes.Add(0.0d, (new AnimatorKeyFrame { Value = default(T) }, true));
} }
if (!hasEndKey) 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"/>. /// A Cue object for <see cref="KeyFrame"/>.
/// </summary> /// </summary>
[TypeConverter(typeof(CueTypeConverter))] [TypeConverter(typeof(CueTypeConverter))]
public struct Cue : IEquatable<Cue>, IEquatable<double> public readonly struct Cue : IEquatable<Cue>, IEquatable<double>
{ {
/// <summary> /// <summary>
/// The normalized percent value, ranging from 0.0 to 1.0 /// 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 firstKF = pair.KFPair.FirstKeyFrame;
var secondKF = pair.KFPair.SecondKeyFrame; var secondKF = pair.KFPair.SecondKeyFrame;
if (firstKF.Value.isNeutral) if (firstKF.isNeutral)
y0 = neutralValue; y0 = neutralValue;
else else
y0 = firstKF.Value.TargetValue; y0 = firstKF.TargetValue;
if (secondKF.Value.isNeutral) if (secondKF.isNeutral)
y1 = neutralValue; y1 = neutralValue;
else else
y1 = secondKF.Value.TargetValue; y1 = secondKF.TargetValue;
// Do linear parametric interpolation // Do linear parametric interpolation
return y0 + (pair.IntraKFTime) * (y1 - y0); return y0 + (pair.IntraKFTime) * (y1 - y0);

10
src/Avalonia.Animation/IAnimation.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using System.Threading.Tasks;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
@ -12,6 +13,11 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Apply the animation to the specified control /// Apply the animation to the specified control
/// </summary> /// </summary>
IDisposable Apply(Animatable control, IObservable<bool> match); IDisposable Apply(Animatable control, IObservable<bool> match, Action onComplete = null);
/// <summary>
/// Run the animation to the specified control
/// </summary>
Task RunAsync(Animatable control);
} }
} }

2
src/Avalonia.Animation/IAnimationSetter.cs

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

2
src/Avalonia.Animation/IAnimator.cs

@ -17,6 +17,6 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Applies the current KeyFrame group to the specified control. /// Applies the current KeyFrame group to the specified control.
/// </summary> /// </summary>
IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch); IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch, Action onComplete);
} }
} }

16
src/Avalonia.Animation/KeyFrame.cs

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

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

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

3
src/Avalonia.Base/Avalonia.Base.csproj

@ -8,4 +8,5 @@
<Import Project="..\..\build\Binding.props" /> <Import Project="..\..\build\Binding.props" />
<Import Project="..\..\build\Rx.props" /> <Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\JetBrains.Annotations.props" /> <Import Project="..\..\build\JetBrains.Annotations.props" />
</Project> <Import Project="..\..\build\System.Memory.props" />
</Project>

51
src/Avalonia.Base/AvaloniaObject.cs

@ -22,7 +22,7 @@ namespace Avalonia
/// <remarks> /// <remarks>
/// This class is analogous to DependencyObject in WPF. /// This class is analogous to DependencyObject in WPF.
/// </remarks> /// </remarks>
public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IPriorityValueOwner public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged
{ {
/// <summary> /// <summary>
/// The parent object that inherited values are inherited from. /// The parent object that inherited values are inherited from.
@ -45,21 +45,8 @@ namespace Avalonia
/// </summary> /// </summary>
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged; private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
private DeferredSetter<AvaloniaProperty, object> _directDeferredSetter;
private ValueStore _values; private ValueStore _values;
private ValueStore Values => _values ?? (_values = new ValueStore(this));
/// <summary>
/// Delayed setter helper for direct properties. Used to fix #855.
/// </summary>
private DeferredSetter<AvaloniaProperty, object> DirectPropertyDeferredSetter
{
get
{
return _directDeferredSetter ??
(_directDeferredSetter = new DeferredSetter<AvaloniaProperty, object>());
}
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AvaloniaObject"/> class. /// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
@ -225,7 +212,7 @@ namespace Avalonia
} }
else if (_values != null) else if (_values != null)
{ {
var result = _values.GetValue(property); var result = Values.GetValue(property);
if (result == AvaloniaProperty.UnsetValue) if (result == AvaloniaProperty.UnsetValue)
{ {
@ -376,12 +363,7 @@ namespace Avalonia
description, description,
priority); priority);
if (_values == null) return Values.AddBinding(property, source, priority);
{
_values = new ValueStore(this);
}
return _values.AddBinding(property, source, priority);
} }
} }
@ -414,9 +396,8 @@ namespace Avalonia
VerifyAccess(); VerifyAccess();
_values?.Revalidate(property); _values?.Revalidate(property);
} }
/// <inheritdoc/> internal void PriorityValueChanged(AvaloniaProperty property, int priority, object oldValue, object newValue)
void IPriorityValueOwner.Changed(AvaloniaProperty property, int priority, object oldValue, object newValue)
{ {
oldValue = (oldValue == AvaloniaProperty.UnsetValue) ? oldValue = (oldValue == AvaloniaProperty.UnsetValue) ?
GetDefaultValue(property) : GetDefaultValue(property) :
@ -439,9 +420,8 @@ namespace Avalonia
(BindingPriority)priority); (BindingPriority)priority);
} }
} }
/// <inheritdoc/> internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
void IPriorityValueOwner.BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
{ {
UpdateDataValidation(property, notification); UpdateDataValidation(property, notification);
} }
@ -456,7 +436,7 @@ namespace Avalonia
/// Gets all priority values set on the object. /// Gets all priority values set on the object.
/// </summary> /// </summary>
/// <returns>A collection of property/value tuples.</returns> /// <returns>A collection of property/value tuples.</returns>
internal IDictionary<AvaloniaProperty, PriorityValue> GetSetValues() => _values?.GetSetValues(); internal IDictionary<AvaloniaProperty, object> GetSetValues() => Values?.GetSetValues();
/// <summary> /// <summary>
/// Forces revalidation of properties when a property value changes. /// Forces revalidation of properties when a property value changes.
@ -566,12 +546,12 @@ namespace Avalonia
T value) T value)
{ {
Contract.Requires<ArgumentNullException>(setterCallback != null); Contract.Requires<ArgumentNullException>(setterCallback != null);
return DirectPropertyDeferredSetter.SetAndNotify( return Values.Setter.SetAndNotify(
property, property,
ref field, ref field,
(object val, ref T backing, Action<Action> notify) => (object update, ref T backing, Action<Action> notify) =>
{ {
setterCallback((T)val, ref backing, notify); setterCallback((T)update, ref backing, notify);
return true; return true;
}, },
value); value);
@ -737,13 +717,8 @@ namespace Avalonia
originalValue?.GetType().FullName ?? "(null)")); originalValue?.GetType().FullName ?? "(null)"));
} }
if (_values == null)
{
_values = new ValueStore(this);
}
LogPropertySet(property, value, priority); LogPropertySet(property, value, priority);
_values.AddValue(property, value, (int)priority); Values.AddValue(property, value, (int)priority);
} }
/// <summary> /// <summary>

52
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -106,7 +106,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Finds a registered non-attached property on a type by name. /// Finds a registered property on a type by name.
/// </summary> /// </summary>
/// <param name="type">The type.</param> /// <param name="type">The type.</param>
/// <param name="name">The property name.</param> /// <param name="name">The property name.</param>
@ -130,7 +130,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Finds a registered non-attached property on a type by name. /// Finds a registered property on an object by name.
/// </summary> /// </summary>
/// <param name="o">The object.</param> /// <param name="o">The object.</param>
/// <param name="name">The property name.</param> /// <param name="name">The property name.</param>
@ -148,52 +148,6 @@ namespace Avalonia
return FindRegistered(o.GetType(), name); 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> /// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is registered on a type. /// Checks whether a <see cref="AvaloniaProperty"/> is registered on a type.
/// </summary> /// </summary>
@ -287,4 +241,4 @@ namespace Avalonia
_attachedCache.Clear(); _attachedCache.Clear();
} }
} }
} }

49
src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs

@ -0,0 +1,49 @@
using System;
using System.Globalization;
namespace Avalonia.Data.Converters
{
/// <summary>
/// A value converter which calls <see cref="string.Format(string, object)"/>
/// </summary>
public class StringFormatValueConverter : IValueConverter
{
/// <summary>
/// Initializes a new instance of the <see cref="StringFormatValueConverter"/> class.
/// </summary>
/// <param name="format">The format string.</param>
/// <param name="inner">
/// An optional inner converter to be called before the format takes place.
/// </param>
public StringFormatValueConverter(string format, IValueConverter inner)
{
Contract.Requires<ArgumentNullException>(format != null);
Format = format;
Inner = inner;
}
/// <summary>
/// Gets an inner value converter which will be called before the string format takes place.
/// </summary>
public IValueConverter Inner { get; }
/// <summary>
/// Gets the format string.
/// </summary>
public string Format { get; }
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
value = Inner?.Convert(value, targetType, parameter, culture) ?? value;
return string.Format(culture, Format, value);
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException("Two way bindings are not supported with a string format");
}
}
}

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

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

3
src/Avalonia.Base/IPriorityValueOwner.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Utilities;
namespace Avalonia namespace Avalonia
{ {
@ -31,5 +32,7 @@ namespace Avalonia
/// Ensures that the current thread is the UI thread. /// Ensures that the current thread is the UI thread.
/// </summary> /// </summary>
void VerifyAccess(); void VerifyAccess();
DeferredSetter<object> Setter { get; }
} }
} }

11
src/Avalonia.Base/PriorityValue.cs

@ -21,7 +21,7 @@ namespace Avalonia
/// priority binding that doesn't return <see cref="AvaloniaProperty.UnsetValue"/>. Where there /// priority binding that doesn't return <see cref="AvaloniaProperty.UnsetValue"/>. Where there
/// are multiple bindings registered with the same priority, the most recently added binding /// are multiple bindings registered with the same priority, the most recently added binding
/// has a higher priority. Each time the value changes, the /// has a higher priority. Each time the value changes, the
/// <see cref="IPriorityValueOwner.Changed(PriorityValue, object, object)"/> method on the /// <see cref="IPriorityValueOwner.Changed"/> method on the
/// owner object is fired with the old and new values. /// owner object is fired with the old and new values.
/// </remarks> /// </remarks>
internal class PriorityValue internal class PriorityValue
@ -30,7 +30,6 @@ namespace Avalonia
private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>(); private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
private readonly Func<object, object> _validate; private readonly Func<object, object> _validate;
private static readonly DeferredSetter<PriorityValue, (object value, int priority)> delayedSetter = new DeferredSetter<PriorityValue, (object, int)>();
private (object value, int priority) _value; private (object value, int priority) _value;
/// <summary> /// <summary>
@ -243,12 +242,18 @@ namespace Avalonia
/// <param name="priority">The priority level that the value came from.</param> /// <param name="priority">The priority level that the value came from.</param>
private void UpdateValue(object value, int priority) private void UpdateValue(object value, int priority)
{ {
delayedSetter.SetAndNotify(this, Owner.Setter.SetAndNotify(Property,
ref _value, ref _value,
UpdateCore, UpdateCore,
(value, priority)); (value, priority));
} }
private bool UpdateCore(
object update,
ref (object value, int priority) backing,
Action<Action> notify)
=> UpdateCore(((object, int))update, ref backing, notify);
private bool UpdateCore( private bool UpdateCore(
(object value, int priority) update, (object value, int priority) update,
ref (object value, int priority) backing, ref (object value, int priority) backing,

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

@ -5,13 +5,13 @@ using System;
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
namespace Avalonia.Markup.Parsers namespace Avalonia.Utilities
{ {
internal ref struct Reader public ref struct CharacterReader
{ {
private ReadOnlySpan<char> _s; private ReadOnlySpan<char> _s;
public Reader(ReadOnlySpan<char> s) public CharacterReader(ReadOnlySpan<char> s)
:this() :this()
{ {
_s = s; _s = s;

53
src/Avalonia.Base/Utilities/DeferredSetter.cs

@ -8,11 +8,10 @@ namespace Avalonia.Utilities
{ {
/// <summary> /// <summary>
/// A utility class to enable deferring assignment until after property-changed notifications are sent. /// A utility class to enable deferring assignment until after property-changed notifications are sent.
/// Used to fix #855.
/// </summary> /// </summary>
/// <typeparam name="TProperty">The type of the object that represents the property.</typeparam>
/// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam> /// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
class DeferredSetter<TProperty, TSetRecord> class DeferredSetter<TSetRecord>
where TProperty: class
{ {
private struct NotifyDisposable : IDisposable private struct NotifyDisposable : IDisposable
{ {
@ -37,29 +36,44 @@ namespace Avalonia.Utilities
{ {
public bool Notifying { get; set; } public bool Notifying { get; set; }
private Queue<TSetRecord> pendingValues; private SingleOrQueue<TSetRecord> pendingValues;
public Queue<TSetRecord> PendingValues public SingleOrQueue<TSetRecord> PendingValues
{ {
get get
{ {
return pendingValues ?? (pendingValues = new Queue<TSetRecord>()); return pendingValues ?? (pendingValues = new SingleOrQueue<TSetRecord>());
} }
} }
} }
private readonly ConditionalWeakTable<TProperty, SettingStatus> setRecords = new ConditionalWeakTable<TProperty, SettingStatus>(); private Dictionary<AvaloniaProperty, SettingStatus> _setRecords;
private Dictionary<AvaloniaProperty, SettingStatus> SetRecords
=> _setRecords ?? (_setRecords = new Dictionary<AvaloniaProperty, SettingStatus>());
private SettingStatus GetOrCreateStatus(AvaloniaProperty property)
{
if (!SetRecords.TryGetValue(property, out var status))
{
status = new SettingStatus();
SetRecords.Add(property, status);
}
return status;
}
/// <summary> /// <summary>
/// Mark the property as currently notifying. /// Mark the property as currently notifying.
/// </summary> /// </summary>
/// <param name="property">The property to mark as notifying.</param> /// <param name="property">The property to mark as notifying.</param>
/// <returns>Returns a disposable that when disposed, marks the property as done notifying.</returns> /// <returns>Returns a disposable that when disposed, marks the property as done notifying.</returns>
private NotifyDisposable MarkNotifying(TProperty property) private NotifyDisposable MarkNotifying(AvaloniaProperty property)
{ {
Contract.Requires<InvalidOperationException>(!IsNotifying(property)); Contract.Requires<InvalidOperationException>(!IsNotifying(property));
return new NotifyDisposable(setRecords.GetOrCreateValue(property)); SettingStatus status = GetOrCreateStatus(property);
return new NotifyDisposable(status);
} }
/// <summary> /// <summary>
@ -67,19 +81,19 @@ namespace Avalonia.Utilities
/// </summary> /// </summary>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <returns>If the property is currently notifying listeners.</returns> /// <returns>If the property is currently notifying listeners.</returns>
private bool IsNotifying(TProperty property) private bool IsNotifying(AvaloniaProperty property)
=> setRecords.TryGetValue(property, out var value) && value.Notifying; => SetRecords.TryGetValue(property, out var value) && value.Notifying;
/// <summary> /// <summary>
/// Add a pending assignment for the property. /// Add a pending assignment for the property.
/// </summary> /// </summary>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="value">The value to assign.</param> /// <param name="value">The value to assign.</param>
private void AddPendingSet(TProperty property, TSetRecord value) private void AddPendingSet(AvaloniaProperty property, TSetRecord value)
{ {
Contract.Requires<InvalidOperationException>(IsNotifying(property)); Contract.Requires<InvalidOperationException>(IsNotifying(property));
setRecords.GetOrCreateValue(property).PendingValues.Enqueue(value); GetOrCreateStatus(property).PendingValues.Enqueue(value);
} }
/// <summary> /// <summary>
@ -87,9 +101,9 @@ namespace Avalonia.Utilities
/// </summary> /// </summary>
/// <param name="property">The property to check.</param> /// <param name="property">The property to check.</param>
/// <returns>If the property has any pending assignments.</returns> /// <returns>If the property has any pending assignments.</returns>
private bool HasPendingSet(TProperty property) private bool HasPendingSet(AvaloniaProperty property)
{ {
return setRecords.TryGetValue(property, out var status) && status.PendingValues.Count != 0; return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty;
} }
/// <summary> /// <summary>
@ -97,9 +111,9 @@ namespace Avalonia.Utilities
/// </summary> /// </summary>
/// <param name="property">The property to check.</param> /// <param name="property">The property to check.</param>
/// <returns>The first pending assignment for the property.</returns> /// <returns>The first pending assignment for the property.</returns>
private TSetRecord GetFirstPendingSet(TProperty property) private TSetRecord GetFirstPendingSet(AvaloniaProperty property)
{ {
return setRecords.GetOrCreateValue(property).PendingValues.Dequeue(); return GetOrCreateStatus(property).PendingValues.Dequeue();
} }
public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback); public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback);
@ -115,7 +129,7 @@ namespace Avalonia.Utilities
/// </param> /// </param>
/// <param name="value">The value to try to set.</param> /// <param name="value">The value to try to set.</param>
public bool SetAndNotify<TValue>( public bool SetAndNotify<TValue>(
TProperty property, AvaloniaProperty property,
ref TValue backing, ref TValue backing,
SetterDelegate<TValue> setterCallback, SetterDelegate<TValue> setterCallback,
TSetRecord value) TSetRecord value)
@ -144,6 +158,7 @@ namespace Avalonia.Utilities
} }
}); });
} }
return updated; return updated;
} }
else if(!object.Equals(value, backing)) else if(!object.Equals(value, backing))

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

@ -6,11 +6,11 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
namespace Avalonia.Markup.Parsers namespace Avalonia.Utilities
{ {
internal static class IdentifierParser public static class IdentifierParser
{ {
public static ReadOnlySpan<char> ParseIdentifier(this ref Reader r) public static ReadOnlySpan<char> ParseIdentifier(this ref CharacterReader r)
{ {
if (IsValidIdentifierStart(r.Peek)) if (IsValidIdentifierStart(r.Peek))
{ {

58
src/Avalonia.Base/Utilities/SingleOrQueue.cs

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Utilities
{
/// <summary>
/// FIFO Queue optimized for holding zero or one items.
/// </summary>
/// <typeparam name="T">The type of items held in the queue.</typeparam>
public class SingleOrQueue<T>
{
private T _head;
private Queue<T> _tail;
private Queue<T> Tail => _tail ?? (_tail = new Queue<T>());
private bool HasTail => _tail != null;
public bool Empty { get; private set; } = true;
public void Enqueue(T value)
{
if (Empty)
{
_head = value;
}
else
{
Tail.Enqueue(value);
}
Empty = false;
}
public T Dequeue()
{
if (Empty)
{
throw new InvalidOperationException("Cannot dequeue from an empty queue!");
}
var result = _head;
if (HasTail && Tail.Count != 0)
{
_head = Tail.Dequeue();
}
else
{
_head = default;
Empty = true;
}
return result;
}
}
}

24
src/Avalonia.Base/ValueStore.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Utilities;
namespace Avalonia namespace Avalonia
{ {
@ -91,15 +92,15 @@ namespace Avalonia
public void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) public void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
{ {
((IPriorityValueOwner)_owner).BindingNotificationReceived(property, notification); _owner.BindingNotificationReceived(property, notification);
} }
public void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue) public void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue)
{ {
((IPriorityValueOwner)_owner).Changed(property, priority, oldValue, newValue); _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) public object GetValue(AvaloniaProperty property)
{ {
@ -115,7 +116,7 @@ namespace Avalonia
public bool IsAnimating(AvaloniaProperty property) public bool IsAnimating(AvaloniaProperty property)
{ {
return _values.TryGetValue(property, out var value) ? (value as PriorityValue)?.IsAnimating ?? false : false; return _values.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
} }
public bool IsSet(AvaloniaProperty property) public bool IsSet(AvaloniaProperty property)
@ -148,13 +149,11 @@ namespace Avalonia
validate2 = v => validate(_owner, v); validate2 = v => validate(_owner, v);
} }
PriorityValue result = new PriorityValue( return new PriorityValue(
this, this,
property, property,
property.PropertyType, property.PropertyType,
validate2); validate2);
return result;
} }
private object Validate(AvaloniaProperty property, object value) private object Validate(AvaloniaProperty property, object value)
@ -168,5 +167,16 @@ namespace Avalonia
return value; return value;
} }
private DeferredSetter<object> _defferedSetter;
public DeferredSetter<object> Setter
{
get
{
return _defferedSetter ??
(_defferedSetter = new DeferredSetter<object>());
}
}
} }
} }

71
src/Avalonia.Controls/ContextMenu.cs

@ -7,11 +7,20 @@ namespace Avalonia.Controls
using System; using System;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Linq; using System.Linq;
using System.ComponentModel;
public class ContextMenu : SelectingItemsControl public class ContextMenu : SelectingItemsControl
{ {
private bool _isOpen; private bool _isOpen;
private Popup _popup; private Popup _popup;
/// <summary>
/// Defines the <see cref="IsOpen"/> property.
/// </summary>
public static readonly DirectProperty<ContextMenu, bool> IsOpenProperty =
AvaloniaProperty.RegisterDirect<ContextMenu, bool>(nameof(IsOpen), o => o.IsOpen);
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ContextMenu"/> class. /// Initializes static members of the <see cref="ContextMenu"/> class.
/// </summary> /// </summary>
@ -22,6 +31,26 @@ namespace Avalonia.Controls
MenuItem.ClickEvent.AddClassHandler<ContextMenu>(x => x.OnContextMenuClick, handledEventsToo: true); MenuItem.ClickEvent.AddClassHandler<ContextMenu>(x => x.OnContextMenuClick, handledEventsToo: true);
} }
/// <summary>
/// Gets a value indicating whether the popup is open
/// </summary>
public bool IsOpen => _isOpen;
/// <summary>
/// Occurs when the value of the
/// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" />
/// property is changing from false to true.
/// </summary>
public event CancelEventHandler ContextMenuOpening;
/// <summary>
/// Occurs when the value of the
/// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" />
/// property is changing from true to false.
/// </summary>
public event CancelEventHandler ContextMenuClosing;
/// <summary> /// <summary>
/// Called when the <see cref="Control.ContextMenu"/> property changes on a control. /// Called when the <see cref="Control.ContextMenu"/> property changes on a control.
/// </summary> /// </summary>
@ -59,12 +88,12 @@ namespace Avalonia.Controls
{ {
if (_popup != null && _popup.IsVisible) if (_popup != null && _popup.IsVisible)
{ {
_popup.Close(); _popup.IsOpen = false;
} }
SelectedIndex = -1; SelectedIndex = -1;
_isOpen = false; SetAndRaise(IsOpenProperty, ref _isOpen, false);
} }
/// <summary> /// <summary>
@ -89,11 +118,11 @@ namespace Avalonia.Controls
} }
((ISetLogicalParent)_popup).SetParent(control); ((ISetLogicalParent)_popup).SetParent(control);
_popup.Child = control.ContextMenu; _popup.Child = this;
_popup.Open(); _popup.IsOpen = true;
control.ContextMenu._isOpen = true; SetAndRaise(IsOpenProperty, ref _isOpen, true);
} }
} }
@ -118,21 +147,37 @@ namespace Avalonia.Controls
var control = (Control)sender; var control = (Control)sender;
var contextMenu = control.ContextMenu; var contextMenu = control.ContextMenu;
if (e.MouseButton == MouseButton.Right) if (control.ContextMenu._isOpen)
{ {
if (control.ContextMenu._isOpen) if (contextMenu.CancelClosing())
{ return;
control.ContextMenu.Hide();
}
contextMenu.Show(control); control.ContextMenu.Hide();
e.Handled = true; e.Handled = true;
} }
else if (contextMenu._isOpen)
if (e.MouseButton == MouseButton.Right)
{ {
control.ContextMenu.Hide(); if (contextMenu.CancelOpening())
return;
contextMenu.Show(control);
e.Handled = true; e.Handled = true;
} }
} }
private bool CancelClosing()
{
var eventArgs = new CancelEventArgs();
ContextMenuClosing?.Invoke(this, eventArgs);
return eventArgs.Cancel;
}
private bool CancelOpening()
{
var eventArgs = new CancelEventArgs();
ContextMenuOpening?.Invoke(this, eventArgs);
return eventArgs.Cancel;
}
} }
} }

6
src/Avalonia.Controls/Expander.cs

@ -66,9 +66,7 @@ namespace Avalonia.Controls
protected virtual void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e) protected virtual void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e)
{ {
IVisual visualContent = Presenter; if (Content != null && ContentTransition != null && Presenter is Visual visualContent)
if (Content != null && ContentTransition != null && visualContent != null)
{ {
bool forward = ExpandDirection == ExpandDirection.Left || bool forward = ExpandDirection == ExpandDirection.Left ||
ExpandDirection == ExpandDirection.Up; ExpandDirection == ExpandDirection.Up;
@ -87,4 +85,4 @@ namespace Avalonia.Controls
private ExpandDirection _expandDirection; private ExpandDirection _expandDirection;
private bool _isExpanded; private bool _isExpanded;
} }
} }

20
src/Avalonia.Controls/Grid.cs

@ -194,6 +194,16 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
private GridLayout.MeasureResult _rowMeasureCache; private GridLayout.MeasureResult _rowMeasureCache;
/// <summary>
/// Gets the row layout as of the last measure.
/// </summary>
private GridLayout _rowLayoutCache;
/// <summary>
/// Gets the column layout as of the last measure.
/// </summary>
private GridLayout _columnLayoutCache;
/// <summary> /// <summary>
/// Measures the grid. /// Measures the grid.
/// </summary> /// </summary>
@ -253,6 +263,9 @@ namespace Avalonia.Controls
// Cache the measure result and return the desired size. // Cache the measure result and return the desired size.
_columnMeasureCache = columnResult; _columnMeasureCache = columnResult;
_rowMeasureCache = rowResult; _rowMeasureCache = rowResult;
_rowLayoutCache = rowLayout;
_columnLayoutCache = columnLayout;
return new Size(columnResult.DesiredLength, rowResult.DesiredLength); return new Size(columnResult.DesiredLength, rowResult.DesiredLength);
// Measure each child only once. // Measure each child only once.
@ -299,13 +312,11 @@ namespace Avalonia.Controls
// arrow back to any statements and re-run them without any side-effect. // arrow back to any statements and re-run them without any side-effect.
var (safeColumns, safeRows) = GetSafeColumnRows(); var (safeColumns, safeRows) = GetSafeColumnRows();
var columnLayout = new GridLayout(ColumnDefinitions); var columnLayout = _columnLayoutCache;
var rowLayout = new GridLayout(RowDefinitions); var rowLayout = _rowLayoutCache;
// Calculate for arrange result. // Calculate for arrange result.
var columnResult = columnLayout.Arrange(finalSize.Width, _columnMeasureCache); var columnResult = columnLayout.Arrange(finalSize.Width, _columnMeasureCache);
var rowResult = rowLayout.Arrange(finalSize.Height, _rowMeasureCache); var rowResult = rowLayout.Arrange(finalSize.Height, _rowMeasureCache);
// Arrange the children. // Arrange the children.
foreach (var child in Children.OfType<Control>()) foreach (var child in Children.OfType<Control>())
{ {
@ -315,7 +326,6 @@ namespace Avalonia.Controls
var y = Enumerable.Range(0, row).Sum(r => rowResult.LengthList[r]); var y = Enumerable.Range(0, row).Sum(r => rowResult.LengthList[r]);
var width = Enumerable.Range(column, columnSpan).Sum(c => columnResult.LengthList[c]); var width = Enumerable.Range(column, columnSpan).Sum(c => columnResult.LengthList[c]);
var height = Enumerable.Range(row, rowSpan).Sum(r => rowResult.LengthList[r]); var height = Enumerable.Range(row, rowSpan).Sum(r => rowResult.LengthList[r]);
child.Arrange(new Rect(x, y, width, height)); child.Arrange(new Rect(x, y, width, height));
} }

9
src/Avalonia.Controls/Image.cs

@ -1,12 +1,11 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // 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;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Displays a <see cref="Bitmap"/> image. /// Displays a <see cref="Bitmap"/> image.
/// </summary> /// </summary>
@ -68,7 +67,9 @@ namespace Avalonia.Controls
Rect sourceRect = new Rect(sourceSize) Rect sourceRect = new Rect(sourceSize)
.CenterRect(new Rect(destRect.Size / scale)); .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
} }
} }
} }
} }

5
src/Avalonia.Controls/Presenters/ItemVirtualizer.cs

@ -161,6 +161,11 @@ namespace Avalonia.Controls.Presenters
/// <returns>An <see cref="ItemVirtualizer"/>.</returns> /// <returns>An <see cref="ItemVirtualizer"/>.</returns>
public static ItemVirtualizer Create(ItemsPresenter owner) public static ItemVirtualizer Create(ItemsPresenter owner)
{ {
if (owner.Panel == null)
{
return null;
}
var virtualizingPanel = owner.Panel as IVirtualizingPanel; var virtualizingPanel = owner.Panel as IVirtualizingPanel;
var scrollable = (ILogicalScrollable)owner; var scrollable = (ILogicalScrollable)owner;
ItemVirtualizer result = null; ItemVirtualizer result = null;

36
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@ -22,7 +22,6 @@ namespace Avalonia.Controls.Presenters
nameof(VirtualizationMode), nameof(VirtualizationMode),
defaultValue: ItemVirtualizationMode.None); defaultValue: ItemVirtualizationMode.None);
private ItemVirtualizer _virtualizer;
private bool _canHorizontallyScroll; private bool _canHorizontallyScroll;
private bool _canVerticallyScroll; private bool _canVerticallyScroll;
@ -76,21 +75,27 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/> /// <inheritdoc/>
bool ILogicalScrollable.IsLogicalScrollEnabled bool ILogicalScrollable.IsLogicalScrollEnabled
{ {
get { return _virtualizer?.IsLogicalScrollEnabled ?? false; } get { return Virtualizer?.IsLogicalScrollEnabled ?? false; }
} }
/// <inheritdoc/> /// <inheritdoc/>
Size IScrollable.Extent => _virtualizer.Extent; Size IScrollable.Extent => Virtualizer?.Extent ?? Size.Empty;
/// <inheritdoc/> /// <inheritdoc/>
Vector IScrollable.Offset Vector IScrollable.Offset
{ {
get { return _virtualizer.Offset; } get { return Virtualizer?.Offset ?? new Vector(); }
set { _virtualizer.Offset = CoerceOffset(value); } set
{
if (Virtualizer != null)
{
Virtualizer.Offset = CoerceOffset(value);
}
}
} }
/// <inheritdoc/> /// <inheritdoc/>
Size IScrollable.Viewport => _virtualizer.Viewport; Size IScrollable.Viewport => Virtualizer?.Viewport ?? Bounds.Size;
/// <inheritdoc/> /// <inheritdoc/>
Action ILogicalScrollable.InvalidateScroll { get; set; } Action ILogicalScrollable.InvalidateScroll { get; set; }
@ -101,6 +106,8 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/> /// <inheritdoc/>
Size ILogicalScrollable.PageScrollSize => new Size(0, 1); Size ILogicalScrollable.PageScrollSize => new Size(0, 1);
internal ItemVirtualizer Virtualizer { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>
bool ILogicalScrollable.BringIntoView(IControl target, Rect targetRect) bool ILogicalScrollable.BringIntoView(IControl target, Rect targetRect)
{ {
@ -110,29 +117,30 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/> /// <inheritdoc/>
IControl ILogicalScrollable.GetControlInDirection(NavigationDirection direction, IControl from) IControl ILogicalScrollable.GetControlInDirection(NavigationDirection direction, IControl from)
{ {
return _virtualizer?.GetControlInDirection(direction, from); return Virtualizer?.GetControlInDirection(direction, from);
} }
public override void ScrollIntoView(object item) public override void ScrollIntoView(object item)
{ {
_virtualizer?.ScrollIntoView(item); Virtualizer?.ScrollIntoView(item);
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
return _virtualizer?.MeasureOverride(availableSize) ?? Size.Empty; return Virtualizer?.MeasureOverride(availableSize) ?? Size.Empty;
} }
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
return _virtualizer?.ArrangeOverride(finalSize) ?? Size.Empty; return Virtualizer?.ArrangeOverride(finalSize) ?? Size.Empty;
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void PanelCreated(IPanel panel) protected override void PanelCreated(IPanel panel)
{ {
_virtualizer = ItemVirtualizer.Create(this); Virtualizer?.Dispose();
Virtualizer = ItemVirtualizer.Create(this);
((ILogicalScrollable)this).InvalidateScroll?.Invoke(); ((ILogicalScrollable)this).InvalidateScroll?.Invoke();
if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty)) if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty))
@ -149,7 +157,7 @@ namespace Avalonia.Controls.Presenters
protected override void ItemsChanged(NotifyCollectionChangedEventArgs e) protected override void ItemsChanged(NotifyCollectionChangedEventArgs e)
{ {
_virtualizer?.ItemsChanged(Items, e); Virtualizer?.ItemsChanged(Items, e);
} }
private Vector CoerceOffset(Vector value) private Vector CoerceOffset(Vector value)
@ -162,8 +170,8 @@ namespace Avalonia.Controls.Presenters
private void VirtualizationModeChanged(AvaloniaPropertyChangedEventArgs e) private void VirtualizationModeChanged(AvaloniaPropertyChangedEventArgs e)
{ {
_virtualizer?.Dispose(); Virtualizer?.Dispose();
_virtualizer = ItemVirtualizer.Create(this); Virtualizer = ItemVirtualizer.Create(this);
((ILogicalScrollable)this).InvalidateScroll?.Invoke(); ((ILogicalScrollable)this).InvalidateScroll?.Invoke();
} }
} }

30
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@ -4,6 +4,7 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Specialized; using System.Collections.Specialized;
using Avalonia.Collections;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Styling; using Avalonia.Styling;
@ -40,6 +41,7 @@ namespace Avalonia.Controls.Presenters
ItemsControl.MemberSelectorProperty.AddOwner<ItemsPresenterBase>(); ItemsControl.MemberSelectorProperty.AddOwner<ItemsPresenterBase>();
private IEnumerable _items; private IEnumerable _items;
private IDisposable _itemsSubscription;
private bool _createdPanel; private bool _createdPanel;
private IItemContainerGenerator _generator; private IItemContainerGenerator _generator;
@ -63,24 +65,12 @@ namespace Avalonia.Controls.Presenters
set set
{ {
if (_createdPanel) _itemsSubscription?.Dispose();
{ _itemsSubscription = null;
INotifyCollectionChanged incc = _items as INotifyCollectionChanged;
if (incc != null)
{
incc.CollectionChanged -= ItemsCollectionChanged;
}
}
if (_createdPanel && value != null) if (_createdPanel && value is INotifyCollectionChanged incc)
{ {
INotifyCollectionChanged incc = value as INotifyCollectionChanged; _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
if (incc != null)
{
incc.CollectionChanged += ItemsCollectionChanged;
}
} }
SetAndRaise(ItemsProperty, ref _items, value); SetAndRaise(ItemsProperty, ref _items, value);
@ -233,11 +223,9 @@ namespace Avalonia.Controls.Presenters
_createdPanel = true; _createdPanel = true;
INotifyCollectionChanged incc = Items as INotifyCollectionChanged; if (_itemsSubscription == null && Items is INotifyCollectionChanged incc)
if (incc != null)
{ {
incc.CollectionChanged += ItemsCollectionChanged; _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
} }
PanelCreated(Panel); PanelCreated(Panel);
@ -263,4 +251,4 @@ namespace Avalonia.Controls.Presenters
(e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this); (e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this);
} }
} }
} }

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

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

25
src/Avalonia.Controls/Primitives/PopupRoot.cs

@ -83,21 +83,22 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
public void SnapInsideScreenEdges() public void SnapInsideScreenEdges()
{ {
var window = this.GetSelfAndLogicalAncestors().OfType<Window>().First(); var screen = Application.Current.MainWindow?.Screens.ScreenFromPoint(Position);
var screen = window.Screens.ScreenFromPoint(Position);
var screenX = Position.X + Bounds.Width - screen.Bounds.X; if (screen != null)
var screenY = Position.Y + Bounds.Height - screen.Bounds.Y;
if (screenX > screen.Bounds.Width)
{ {
Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width)); var screenX = Position.X + Bounds.Width - screen.Bounds.X;
} var screenY = Position.Y + Bounds.Height - screen.Bounds.Y;
if (screenY > screen.Bounds.Height) if (screenX > screen.Bounds.Width)
{ {
Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height)); Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width));
}
if (screenY > screen.Bounds.Height)
{
Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height));
}
} }
} }

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

@ -360,7 +360,7 @@ namespace Avalonia.Controls.Primitives
{ {
if (!AlwaysSelected) if (!AlwaysSelected)
{ {
SelectedIndex = -1; selectedIndex = SelectedIndex = -1;
} }
else else
{ {
@ -368,10 +368,15 @@ namespace Avalonia.Controls.Primitives
} }
} }
var items = Items?.Cast<object>();
if (selectedIndex >= items.Count())
{
selectedIndex = SelectedIndex = items.Count() - 1;
}
break; break;
case NotifyCollectionChangedAction.Reset: case NotifyCollectionChangedAction.Reset:
SelectedIndex = IndexOf(e.NewItems, SelectedItem); SelectedIndex = IndexOf(Items, SelectedItem);
break; break;
} }
} }

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

@ -14,7 +14,7 @@ namespace Avalonia.Controls.Primitives
nameof(IsChecked), nameof(IsChecked),
o => o.IsChecked, o => o.IsChecked,
(o, v) => o.IsChecked = v, (o, v) => o.IsChecked = v,
unsetValue: false, unsetValue: null,
defaultBindingMode: BindingMode.TwoWay); defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty<bool> IsThreeStateProperty = public static readonly StyledProperty<bool> IsThreeStateProperty =

90
src/Avalonia.Controls/ProgressBar.cs

@ -21,18 +21,27 @@ namespace Avalonia.Controls
public static readonly StyledProperty<Orientation> OrientationProperty = public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<ProgressBar, Orientation>(nameof(Orientation), Orientation.Horizontal); 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 Border _indicator;
private IndeterminateAnimation _indeterminateAnimation;
static ProgressBar() static ProgressBar()
{ {
PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical"); PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical");
PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal"); PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
PseudoClass(IsIndeterminateProperty, ":indeterminate");
ValueProperty.Changed.AddClassHandler<ProgressBar>(x => x.ValueChanged); 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 public bool IsIndeterminate
@ -46,6 +55,19 @@ namespace Avalonia.Controls
get => GetValue(OrientationProperty); get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value); 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/> /// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
@ -60,7 +82,6 @@ namespace Avalonia.Controls
_indicator = e.NameScope.Get<Border>("PART_Indicator"); _indicator = e.NameScope.Get<Border>("PART_Indicator");
UpdateIndicator(Bounds.Size); UpdateIndicator(Bounds.Size);
UpdateIsIndeterminate(IsIndeterminate);
} }
private void UpdateIndicator(Size bounds) private void UpdateIndicator(Size bounds)
@ -70,9 +91,20 @@ namespace Avalonia.Controls
if (IsIndeterminate) if (IsIndeterminate)
{ {
if (Orientation == Orientation.Horizontal) 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 else
_indicator.Height = bounds.Height / 5.0; {
var height = bounds.Height / 5.0;
IndeterminateStartingOffset = -bounds.Height;
_indicator.Height = height;
IndeterminateEndingOffset = height;
}
} }
else 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) private void ValueChanged(AvaloniaPropertyChangedEventArgs e)
{ {
UpdateIndicator(Bounds.Size); 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. /// Defines the <see cref="Orientation"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<Orientation> OrientationProperty = public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<Slider, Orientation>(nameof(Orientation), Orientation.Horizontal); ScrollBar.OrientationProperty.AddOwner<Slider>();
/// <summary> /// <summary>
/// Defines the <see cref="IsSnapToTickEnabled"/> property. /// Defines the <see cref="IsSnapToTickEnabled"/> property.
@ -41,8 +41,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
static Slider() static Slider()
{ {
PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical"); OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal);
PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
Thumb.DragStartedEvent.AddClassHandler<Slider>(x => x.OnThumbDragStarted, RoutingStrategies.Bubble); Thumb.DragStartedEvent.AddClassHandler<Slider>(x => x.OnThumbDragStarted, RoutingStrategies.Bubble);
Thumb.DragDeltaEvent.AddClassHandler<Slider>(x => x.OnThumbDragDelta, RoutingStrategies.Bubble); Thumb.DragDeltaEvent.AddClassHandler<Slider>(x => x.OnThumbDragDelta, RoutingStrategies.Bubble);
Thumb.DragCompletedEvent.AddClassHandler<Slider>(x => x.OnThumbDragCompleted, RoutingStrategies.Bubble); Thumb.DragCompletedEvent.AddClassHandler<Slider>(x => x.OnThumbDragCompleted, RoutingStrategies.Bubble);

41
src/Avalonia.Controls/StackPanel.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Linq;
using Avalonia.Input; using Avalonia.Input;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -12,10 +13,10 @@ namespace Avalonia.Controls
public class StackPanel : Panel, INavigableContainer public class StackPanel : Panel, INavigableContainer
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Gap"/> property. /// Defines the <see cref="Spacing"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<double> GapProperty = public static readonly StyledProperty<double> SpacingProperty =
AvaloniaProperty.Register<StackPanel, double>(nameof(Gap)); AvaloniaProperty.Register<StackPanel, double>(nameof(Spacing));
/// <summary> /// <summary>
/// Defines the <see cref="Orientation"/> property. /// Defines the <see cref="Orientation"/> property.
@ -28,17 +29,17 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
static StackPanel() static StackPanel()
{ {
AffectsMeasure(GapProperty); AffectsMeasure(SpacingProperty);
AffectsMeasure(OrientationProperty); AffectsMeasure(OrientationProperty);
} }
/// <summary> /// <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> /// </summary>
public double Gap public double Spacing
{ {
get { return GetValue(GapProperty); } get { return GetValue(SpacingProperty); }
set { SetValue(GapProperty, value); } set { SetValue(SpacingProperty, value); }
} }
/// <summary> /// <summary>
@ -151,7 +152,8 @@ namespace Avalonia.Controls
double measuredWidth = 0; double measuredWidth = 0;
double measuredHeight = 0; double measuredHeight = 0;
double gap = Gap; double spacing = Spacing;
bool hasVisibleChild = Children.Any(c => c.IsVisible);
foreach (Control child in Children) foreach (Control child in Children)
{ {
@ -160,23 +162,23 @@ namespace Avalonia.Controls
if (Orientation == Orientation.Vertical) if (Orientation == Orientation.Vertical)
{ {
measuredHeight += size.Height + gap; measuredHeight += size.Height + (child.IsVisible ? spacing : 0);
measuredWidth = Math.Max(measuredWidth, size.Width); measuredWidth = Math.Max(measuredWidth, size.Width);
} }
else else
{ {
measuredWidth += size.Width + gap; measuredWidth += size.Width + (child.IsVisible ? spacing : 0);
measuredHeight = Math.Max(measuredHeight, size.Height); measuredHeight = Math.Max(measuredHeight, size.Height);
} }
} }
if (Orientation == Orientation.Vertical) if (Orientation == Orientation.Vertical)
{ {
measuredHeight -= gap; measuredHeight -= (hasVisibleChild ? spacing : 0);
} }
else else
{ {
measuredWidth -= gap; measuredWidth -= (hasVisibleChild ? spacing : 0);
} }
return new Size(measuredWidth, measuredHeight); return new Size(measuredWidth, measuredHeight);
@ -192,7 +194,8 @@ namespace Avalonia.Controls
var orientation = Orientation; var orientation = Orientation;
double arrangedWidth = finalSize.Width; double arrangedWidth = finalSize.Width;
double arrangedHeight = finalSize.Height; double arrangedHeight = finalSize.Height;
double gap = Gap; double spacing = Spacing;
bool hasVisibleChild = Children.Any(c => c.IsVisible);
if (Orientation == Orientation.Vertical) if (Orientation == Orientation.Vertical)
{ {
@ -214,25 +217,25 @@ namespace Avalonia.Controls
Rect childFinal = new Rect(0, arrangedHeight, width, childHeight); Rect childFinal = new Rect(0, arrangedHeight, width, childHeight);
ArrangeChild(child, childFinal, finalSize, orientation); ArrangeChild(child, childFinal, finalSize, orientation);
arrangedWidth = Math.Max(arrangedWidth, childWidth); arrangedWidth = Math.Max(arrangedWidth, childWidth);
arrangedHeight += childHeight + gap; arrangedHeight += childHeight + (child.IsVisible ? spacing : 0);
} }
else else
{ {
double height = Math.Max(childHeight, arrangedHeight); double height = Math.Max(childHeight, arrangedHeight);
Rect childFinal = new Rect(arrangedWidth, 0, childWidth, height); Rect childFinal = new Rect(arrangedWidth, 0, childWidth, height);
ArrangeChild(child, childFinal, finalSize, orientation); ArrangeChild(child, childFinal, finalSize, orientation);
arrangedWidth += childWidth + gap; arrangedWidth += childWidth + (child.IsVisible ? spacing : 0);
arrangedHeight = Math.Max(arrangedHeight, childHeight); arrangedHeight = Math.Max(arrangedHeight, childHeight);
} }
} }
if (orientation == Orientation.Vertical) if (orientation == Orientation.Vertical)
{ {
arrangedHeight = Math.Max(arrangedHeight - gap, finalSize.Height); arrangedHeight = Math.Max(arrangedHeight - (hasVisibleChild ? spacing : 0), finalSize.Height);
} }
else else
{ {
arrangedWidth = Math.Max(arrangedWidth - gap, finalSize.Width); arrangedWidth = Math.Max(arrangedWidth - (hasVisibleChild ? spacing : 0), finalSize.Width);
} }
return new Size(arrangedWidth, arrangedHeight); return new Size(arrangedWidth, arrangedHeight);
@ -247,4 +250,4 @@ namespace Avalonia.Controls
child.Arrange(rect); child.Arrange(rect);
} }
} }
} }

16
src/Avalonia.Controls/TextBox.cs

@ -124,7 +124,7 @@ namespace Avalonia.Controls
ScrollViewer.HorizontalScrollBarVisibilityProperty, ScrollViewer.HorizontalScrollBarVisibilityProperty,
horizontalScrollBarVisibility, horizontalScrollBarVisibility,
BindingPriority.Style); BindingPriority.Style);
_undoRedoHelper = new UndoRedoHelper<UndoRedoState>(this); _undoRedoHelper = new UndoRedoHelper<UndoRedoState>(this);
} }
public bool AcceptsReturn public bool AcceptsReturn
@ -262,7 +262,7 @@ namespace Avalonia.Controls
if (IsFocused) if (IsFocused)
{ {
_presenter.ShowCaret(); DecideCaretVisibility();
} }
} }
@ -282,12 +282,20 @@ namespace Avalonia.Controls
} }
else else
{ {
_presenter?.ShowCaret(); DecideCaretVisibility();
} }
e.Handled = true; e.Handled = true;
} }
private void DecideCaretVisibility()
{
if (!IsReadOnly)
_presenter?.ShowCaret();
else
_presenter?.HideCaret();
}
protected override void OnLostFocus(RoutedEventArgs e) protected override void OnLostFocus(RoutedEventArgs e)
{ {
base.OnLostFocus(e); base.OnLostFocus(e);
@ -557,7 +565,7 @@ namespace Avalonia.Controls
var index = CaretIndex = _presenter.GetCaretIndex(point); var index = CaretIndex = _presenter.GetCaretIndex(point);
var text = Text; var text = Text;
if (text != null) if (text != null && e.MouseButton == MouseButton.Left)
{ {
switch (e.ClickCount) switch (e.ClickCount)
{ {

12
src/Avalonia.Controls/VirtualizingStackPanel.cs

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

10
src/Avalonia.Controls/Window.cs

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

4
src/Avalonia.Controls/WindowCollection.cs

@ -96,7 +96,7 @@ namespace Avalonia
{ {
while (_windows.Count > 0) 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"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <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>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
@ -37,11 +42,6 @@
<ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" /> <ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" />
<ProjectReference Include="..\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" /> <ProjectReference Include="..\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Include="..\Shared\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
</ItemGroup>
<Import Project="..\..\build\Microsoft.CSharp.props" /> <Import Project="..\..\build\Microsoft.CSharp.props" />
<Import Project="..\..\build\Rx.props" /> <Import Project="..\..\build\Rx.props" />
</Project> </Project>

4
src/Avalonia.Diagnostics/DevTools.xaml

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

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

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

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

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

1
src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj

@ -10,6 +10,7 @@
<ProjectReference Include="..\Gtk\Avalonia.Gtk3\Avalonia.Gtk3.csproj" /> <ProjectReference Include="..\Gtk\Avalonia.Gtk3\Avalonia.Gtk3.csproj" />
<ProjectReference Include="..\OSX\Avalonia.MonoMac\Avalonia.MonoMac.csproj" /> <ProjectReference Include="..\OSX\Avalonia.MonoMac\Avalonia.MonoMac.csproj" />
<ProjectReference Include="..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" /> <ProjectReference Include="..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<ProjectReference Include="..\Windows\Avalonia.Direct2D1\Avalonia.Direct2D1.csproj" />
<ProjectReference Include="..\Windows\Avalonia.Win32\Avalonia.Win32.csproj" /> <ProjectReference Include="..\Windows\Avalonia.Win32\Avalonia.Win32.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\NetCore.props" /> <Import Project="..\..\build\NetCore.props" />

17
src/Avalonia.Styling/Styling/Setter.cs

@ -126,7 +126,7 @@ namespace Avalonia.Styling
if (source != null) 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); return BindingOperations.Apply(control, Property, cloned, null);
} }
} }
@ -134,13 +134,13 @@ namespace Avalonia.Styling
return Disposable.Empty; 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) if (activator != null)
{ {
var description = style?.ToString(); var description = style?.ToString();
switch (sourceInstance.Mode) switch (mode)
{ {
case BindingMode.OneTime: case BindingMode.OneTime:
if (sourceInstance.Observable != null) if (sourceInstance.Observable != null)
@ -158,18 +158,11 @@ namespace Avalonia.Styling
var activated = new ActivatedObservable(activator, sourceInstance.Observable, description); var activated = new ActivatedObservable(activator, sourceInstance.Observable, description);
return InstancedBinding.OneWay(activated, BindingPriority.StyleTrigger); return InstancedBinding.OneWay(activated, BindingPriority.StyleTrigger);
} }
case BindingMode.OneWayToSource: default:
{
var activated = new ActivatedSubject(activator, sourceInstance.Subject, description);
return InstancedBinding.OneWayToSource(activated, BindingPriority.StyleTrigger);
}
case BindingMode.TwoWay:
{ {
var activated = new ActivatedSubject(activator, sourceInstance.Subject, description); var activated = new ActivatedSubject(activator, sourceInstance.Subject, description);
return InstancedBinding.TwoWay(activated, BindingPriority.StyleTrigger); return new InstancedBinding(activated, sourceInstance.Mode, BindingPriority.StyleTrigger);
} }
default:
throw new NotSupportedException("Unsupported BindingMode.");
} }
} }

5
src/Avalonia.Themes.Default/MenuItem.xaml

@ -122,6 +122,11 @@
</Setter> </Setter>
</Style> </Style>
<Style Selector="MenuItem:selected /template/ Border#root">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>
</Style>
<Style Selector="MenuItem:pointerover /template/ Border#root"> <Style Selector="MenuItem:pointerover /template/ Border#root">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/> <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/> <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>

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

@ -7,14 +7,9 @@
<Border Background="{TemplateBinding Background}" <Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"> BorderThickness="{TemplateBinding BorderThickness}">
<Grid> <Border Name="PART_Indicator"
<Border Name="PART_Track" BorderThickness="1"
BorderThickness="1" Background="{TemplateBinding Foreground}"/>
BorderBrush="{TemplateBinding Background}"/>
<Border Name="PART_Indicator"
BorderThickness="1"
Background="{TemplateBinding Foreground}" />
</Grid>
</Border> </Border>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
@ -35,4 +30,36 @@
<Setter Property="MinWidth" Value="14"/> <Setter Property="MinWidth" Value="14"/>
<Setter Property="MinHeight" Value="200"/> <Setter Property="MinHeight" Value="200"/>
</Style> </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>

60
src/Avalonia.Visuals/Animation/CrossFade.cs

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive.Threading.Tasks; using System.Reactive.Threading.Tasks;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Styling;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Animation namespace Avalonia.Animation
@ -14,10 +15,14 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
public class CrossFade : IPageTransition public class CrossFade : IPageTransition
{ {
private Animation _fadeOutAnimation;
private Animation _fadeInAnimation;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="CrossFade"/> class. /// Initializes a new instance of the <see cref="CrossFade"/> class.
/// </summary> /// </summary>
public CrossFade() public CrossFade()
:this(TimeSpan.Zero)
{ {
} }
@ -27,13 +32,51 @@ namespace Avalonia.Animation
/// <param name="duration">The duration of the animation.</param> /// <param name="duration">The duration of the animation.</param>
public CrossFade(TimeSpan duration) public CrossFade(TimeSpan duration)
{ {
Duration = duration; _fadeOutAnimation = new Animation
{
new KeyFrame
(
new Setter
{
Property = Visual.OpacityProperty,
Value = 0.0
}
)
{
Cue = new Cue(1.0)
}
};
_fadeInAnimation = new Animation
{
new KeyFrame
(
new Setter
{
Property = Visual.OpacityProperty,
Value = 0.0
}
)
{
Cue = new Cue(0.0)
}
};
_fadeOutAnimation.Duration = _fadeInAnimation.Duration = duration;
} }
/// <summary> /// <summary>
/// Gets the duration of the animation. /// Gets the duration of the animation.
/// </summary> /// </summary>
public TimeSpan Duration { get; set; } public TimeSpan Duration
{
get
{
return _fadeOutAnimation.Duration;
}
set
{
_fadeOutAnimation.Duration = _fadeInAnimation.Duration = value;
}
}
/// <summary> /// <summary>
/// Starts the animation. /// Starts the animation.
@ -47,12 +90,10 @@ namespace Avalonia.Animation
/// <returns> /// <returns>
/// A <see cref="Task"/> that tracks the progress of the animation. /// A <see cref="Task"/> that tracks the progress of the animation.
/// </returns> /// </returns>
public async Task Start(IVisual from, IVisual to) public async Task Start(Visual from, Visual to)
{ {
var tasks = new List<Task>(); var tasks = new List<Task>();
// TODO: Implement relevant transition logic here (or discard this class)
// in favor of XAML based transition for pages
if (to != null) if (to != null)
{ {
to.Opacity = 0; to.Opacity = 0;
@ -60,22 +101,21 @@ namespace Avalonia.Animation
if (from != null) if (from != null)
{ {
tasks.Add(_fadeOutAnimation.RunAsync(from));
} }
if (to != null) if (to != null)
{ {
to.Opacity = 0;
to.IsVisible = true; to.IsVisible = true;
tasks.Add(_fadeInAnimation.RunAsync(to));
} }
// FIXME: This is temporary until animations are fixed. await Task.WhenAll(tasks);
await Task.Delay(1);
if (from != null) if (from != null)
{ {
from.IsVisible = false; from.IsVisible = false;
from.Opacity = 1;
} }
if (to != null) if (to != null)
@ -99,7 +139,7 @@ namespace Avalonia.Animation
/// <returns> /// <returns>
/// A <see cref="Task"/> that tracks the progress of the animation. /// A <see cref="Task"/> that tracks the progress of the animation.
/// </returns> /// </returns>
Task IPageTransition.Start(IVisual from, IVisual to, bool forward) Task IPageTransition.Start(Visual from, Visual to, bool forward)
{ {
return Start(from, to); return Start(from, to);
} }

2
src/Avalonia.Visuals/Animation/IPageTransition.cs

@ -26,6 +26,6 @@ namespace Avalonia.Animation
/// <returns> /// <returns>
/// A <see cref="Task"/> that tracks the progress of the animation. /// A <see cref="Task"/> that tracks the progress of the animation.
/// </returns> /// </returns>
Task Start(IVisual from, IVisual to, bool forward); Task Start(Visual from, Visual to, bool forward);
} }
} }

64
src/Avalonia.Visuals/Animation/PageSlide.cs

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Reactive.Threading.Tasks; using System.Reactive.Threading.Tasks;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Animation namespace Avalonia.Animation
@ -67,7 +68,7 @@ namespace Avalonia.Animation
/// <returns> /// <returns>
/// A <see cref="Task"/> that tracks the progress of the animation. /// A <see cref="Task"/> that tracks the progress of the animation.
/// </returns> /// </returns>
public async Task Start(IVisual from, IVisual to, bool forward) public async Task Start(Visual from, Visual to, bool forward)
{ {
var tasks = new List<Task>(); var tasks = new List<Task>();
var parent = GetVisualParent(from, to); var parent = GetVisualParent(from, to);
@ -79,16 +80,69 @@ namespace Avalonia.Animation
// in favor of XAML based transition for pages // in favor of XAML based transition for pages
if (from != null) if (from != null)
{ {
var animation = new Animation
{
new KeyFrame
(
new Setter
{
Property = translateProperty,
Value = 0
}
)
{
Cue = new Cue(0.0)
},
new KeyFrame
(
new Setter
{
Property = translateProperty,
Value = forward ? -distance : distance
}
)
{
Cue = new Cue(1.0)
}
};
animation.Duration = Duration;
tasks.Add(animation.RunAsync(from));
} }
if (to != null) if (to != null)
{ {
to.IsVisible = true;
var animation = new Animation
{
new KeyFrame
(
new Setter
{
Property = translateProperty,
Value = forward ? -distance : distance
}
)
{
Cue = new Cue(0.0)
},
new KeyFrame
(
new Setter
{
Property = translateProperty,
Value = 0
}
)
{
Cue = new Cue(1.0)
},
};
animation.Duration = Duration;
tasks.Add(animation.RunAsync(to));
} }
// FIXME: This is temporary until animations are fixed. await Task.WhenAll(tasks);
await Task.Delay(1);
if (from != null) if (from != null)
{ {

6
src/Avalonia.Visuals/Animation/TransformAnimator.cs

@ -19,7 +19,7 @@ namespace Avalonia.Animation
DoubleAnimator childKeyFrames; DoubleAnimator childKeyFrames;
/// <inheritdoc/> /// <inheritdoc/>
public override IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch) public override IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch, Action onComplete)
{ {
var ctrl = (Visual)control; var ctrl = (Visual)control;
@ -51,7 +51,7 @@ namespace Avalonia.Animation
// It's a transform object so let's target that. // It's a transform object so let's target that.
if (renderTransformType == Property.OwnerType) if (renderTransformType == Property.OwnerType)
{ {
return childKeyFrames.Apply(animation, ctrl.RenderTransform, obsMatch); return childKeyFrames.Apply(animation, ctrl.RenderTransform, obsMatch, onComplete);
} }
// It's a TransformGroup and try finding the target there. // It's a TransformGroup and try finding the target there.
else if (renderTransformType == typeof(TransformGroup)) else if (renderTransformType == typeof(TransformGroup))
@ -60,7 +60,7 @@ namespace Avalonia.Animation
{ {
if (transform.GetType() == Property.OwnerType) if (transform.GetType() == Property.OwnerType)
{ {
return childKeyFrames.Apply(animation, transform, obsMatch); return childKeyFrames.Apply(animation, transform, obsMatch, onComplete);
} }
} }
} }

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

@ -2,9 +2,10 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Visuals.Media.Imaging;
namespace Avalonia.Media namespace Avalonia.Media
{ {
public sealed class DrawingContext : IDisposable public sealed class DrawingContext : IDisposable
{ {
private int _currentLevel; private int _currentLevel;
@ -68,11 +69,12 @@ namespace Avalonia.Media
/// <param name="opacity">The opacity to draw with.</param> /// <param name="opacity">The opacity to draw with.</param>
/// <param name="sourceRect">The rect in the image to draw.</param> /// <param name="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</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); Contract.Requires<ArgumentNullException>(source != null);
PlatformImpl.DrawImage(source.PlatformImpl, opacity, sourceRect, destRect); PlatformImpl.DrawImage(source.PlatformImpl, opacity, sourceRect, destRect, bitmapInterpolationMode);
} }
/// <summary> /// <summary>
@ -309,4 +311,4 @@ namespace Avalonia.Media
return pen?.Brush != null && pen.Thickness > 0; 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> /// <summary>
/// A brush which displays a repeating image. /// A brush which displays a repeating image.
/// </summary> /// </summary>
@ -35,5 +40,13 @@
/// Gets the brush's tile mode. /// Gets the brush's tile mode.
/// </summary> /// </summary>
TileMode TileMode { get; } 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 namespace Avalonia.Media.Immutable
{ {
@ -21,6 +21,7 @@ namespace Avalonia.Media.Immutable
/// How the source rectangle will be stretched to fill the destination rect. /// How the source rectangle will be stretched to fill the destination rect.
/// </param> /// </param>
/// <param name="tileMode">The tile mode.</param> /// <param name="tileMode">The tile mode.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
public ImmutableImageBrush( public ImmutableImageBrush(
IBitmap source, IBitmap source,
AlignmentX alignmentX = AlignmentX.Center, AlignmentX alignmentX = AlignmentX.Center,
@ -29,7 +30,8 @@ namespace Avalonia.Media.Immutable
double opacity = 1, double opacity = 1,
RelativeRect? sourceRect = null, RelativeRect? sourceRect = null,
Stretch stretch = Stretch.Uniform, Stretch stretch = Stretch.Uniform,
TileMode tileMode = TileMode.None) TileMode tileMode = TileMode.None,
BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
: base( : base(
alignmentX, alignmentX,
alignmentY, alignmentY,
@ -37,7 +39,8 @@ namespace Avalonia.Media.Immutable
opacity, opacity,
sourceRect ?? RelativeRect.Fill, sourceRect ?? RelativeRect.Fill,
stretch, stretch,
tileMode) tileMode,
bitmapInterpolationMode)
{ {
Source = source; 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 namespace Avalonia.Media.Immutable
{ {
@ -19,6 +22,7 @@ namespace Avalonia.Media.Immutable
/// How the source rectangle will be stretched to fill the destination rect. /// How the source rectangle will be stretched to fill the destination rect.
/// </param> /// </param>
/// <param name="tileMode">The tile mode.</param> /// <param name="tileMode">The tile mode.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
protected ImmutableTileBrush( protected ImmutableTileBrush(
AlignmentX alignmentX, AlignmentX alignmentX,
AlignmentY alignmentY, AlignmentY alignmentY,
@ -26,7 +30,8 @@ namespace Avalonia.Media.Immutable
double opacity, double opacity,
RelativeRect sourceRect, RelativeRect sourceRect,
Stretch stretch, Stretch stretch,
TileMode tileMode) TileMode tileMode,
BitmapInterpolationMode bitmapInterpolationMode)
{ {
AlignmentX = alignmentX; AlignmentX = alignmentX;
AlignmentY = alignmentY; AlignmentY = alignmentY;
@ -35,6 +40,7 @@ namespace Avalonia.Media.Immutable
SourceRect = sourceRect; SourceRect = sourceRect;
Stretch = stretch; Stretch = stretch;
TileMode = tileMode; TileMode = tileMode;
BitmapInterpolationMode = bitmapInterpolationMode;
} }
/// <summary> /// <summary>
@ -49,7 +55,8 @@ namespace Avalonia.Media.Immutable
source.Opacity, source.Opacity,
source.SourceRect, source.SourceRect,
source.Stretch, source.Stretch,
source.TileMode) source.TileMode,
source.BitmapInterpolationMode)
{ {
} }
@ -73,5 +80,8 @@ namespace Avalonia.Media.Immutable
/// <inheritdoc/> /// <inheritdoc/>
public TileMode TileMode { get; } 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; using Avalonia.VisualTree;
namespace Avalonia.Media.Immutable namespace Avalonia.Media.Immutable
@ -21,6 +24,7 @@ namespace Avalonia.Media.Immutable
/// How the source rectangle will be stretched to fill the destination rect. /// How the source rectangle will be stretched to fill the destination rect.
/// </param> /// </param>
/// <param name="tileMode">The tile mode.</param> /// <param name="tileMode">The tile mode.</param>
/// <param name="bitmapInterpolationMode">Controls the quality of interpolation.</param>
public ImmutableVisualBrush( public ImmutableVisualBrush(
IVisual visual, IVisual visual,
AlignmentX alignmentX = AlignmentX.Center, AlignmentX alignmentX = AlignmentX.Center,
@ -29,7 +33,8 @@ namespace Avalonia.Media.Immutable
double opacity = 1, double opacity = 1,
RelativeRect? sourceRect = null, RelativeRect? sourceRect = null,
Stretch stretch = Stretch.Uniform, Stretch stretch = Stretch.Uniform,
TileMode tileMode = TileMode.None) TileMode tileMode = TileMode.None,
BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
: base( : base(
alignmentX, alignmentX,
alignmentY, alignmentY,
@ -37,7 +42,8 @@ namespace Avalonia.Media.Immutable
opacity, opacity,
sourceRect ?? RelativeRect.Fill, sourceRect ?? RelativeRect.Fill,
stretch, stretch,
tileMode) tileMode,
bitmapInterpolationMode)
{ {
Visual = visual; 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. // 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. // 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 namespace Avalonia.Media
{ {
/// <summary> /// <summary>
@ -75,6 +77,11 @@ namespace Avalonia.Media
public static readonly StyledProperty<TileMode> TileModeProperty = public static readonly StyledProperty<TileMode> TileModeProperty =
AvaloniaProperty.Register<TileBrush, TileMode>(nameof(TileMode)); AvaloniaProperty.Register<TileBrush, TileMode>(nameof(TileMode));
static TileBrush()
{
RenderOptions.BitmapInterpolationModeProperty.OverrideDefaultValue<TileBrush>(BitmapInterpolationMode.Default);
}
/// <summary> /// <summary>
/// Gets or sets the horizontal alignment of a tile in the destination. /// Gets or sets the horizontal alignment of a tile in the destination.
/// </summary> /// </summary>
@ -129,5 +136,17 @@ namespace Avalonia.Media
get { return (TileMode)GetValue(TileModeProperty); } get { return (TileMode)GetValue(TileModeProperty); }
set { SetValue(TileModeProperty, value); } 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 System;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.Visuals.Media.Imaging;
namespace Avalonia.Platform namespace Avalonia.Platform
{ {
@ -30,7 +31,8 @@ namespace Avalonia.Platform
/// <param name="opacity">The opacity to draw with.</param> /// <param name="opacity">The opacity to draw with.</param>
/// <param name="sourceRect">The rect in the image to draw.</param> /// <param name="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</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> /// <summary>
/// Draws a bitmap image. /// Draws a bitmap image.

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

@ -7,6 +7,7 @@ using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Avalonia.Visuals.Media.Imaging;
namespace Avalonia.Rendering.SceneGraph namespace Avalonia.Rendering.SceneGraph
{ {
@ -114,13 +115,13 @@ namespace Avalonia.Rendering.SceneGraph
} }
/// <inheritdoc/> /// <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>(); 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 else
{ {

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

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

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

@ -33,17 +33,24 @@ namespace Avalonia.Gtk.Common
{ GdkKey.Prior, Key.Prior }, { GdkKey.Prior, Key.Prior },
//{ GdkKey.?, Key.PageDown } //{ GdkKey.?, Key.PageDown }
{ GdkKey.End, Key.End }, { GdkKey.End, Key.End },
{ GdkKey.KP_End, Key.End },
{ GdkKey.Home, Key.Home }, { GdkKey.Home, Key.Home },
{ GdkKey.KP_Home, Key.Home },
{ GdkKey.Left, Key.Left }, { GdkKey.Left, Key.Left },
{ GdkKey.KP_Left, Key.Left },
{ GdkKey.Up, Key.Up }, { GdkKey.Up, Key.Up },
{ GdkKey.KP_Up, Key.Up },
{ GdkKey.Right, Key.Right }, { GdkKey.Right, Key.Right },
{ GdkKey.KP_Right, Key.Right },
{ GdkKey.Down, Key.Down }, { GdkKey.Down, Key.Down },
{ GdkKey.KP_Down, Key.Down },
{ GdkKey.Select, Key.Select }, { GdkKey.Select, Key.Select },
{ GdkKey.Print, Key.Print }, { GdkKey.Print, Key.Print },
{ GdkKey.Execute, Key.Execute }, { GdkKey.Execute, Key.Execute },
//{ GdkKey.?, Key.Snapshot } //{ GdkKey.?, Key.Snapshot }
{ GdkKey.Insert, Key.Insert }, { GdkKey.Insert, Key.Insert },
{ GdkKey.Delete, Key.Delete }, { GdkKey.Delete, Key.Delete },
{ GdkKey.KP_Delete, Key.Delete },
{ GdkKey.Help, Key.Help }, { GdkKey.Help, Key.Help },
//{ GdkKey.?, Key.D0 } //{ GdkKey.?, Key.D0 }
//{ GdkKey.?, Key.D1 } //{ 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\ResourceInclude.cs" />
<Compile Include="MarkupExtensions\StaticResourceExtension.cs" /> <Compile Include="MarkupExtensions\StaticResourceExtension.cs" />
<Compile Include="MarkupExtensions\StyleIncludeExtension.cs" /> <Compile Include="MarkupExtensions\StyleIncludeExtension.cs" />
<Compile Include="Parsers\PropertyParser.cs" />
<Compile Include="PortableXaml\AvaloniaXamlContext.cs" /> <Compile Include="PortableXaml\AvaloniaXamlContext.cs" />
<Compile Include="PortableXaml\AttributeExtensions.cs" /> <Compile Include="PortableXaml\AttributeExtensions.cs" />
<Compile Include="PortableXaml\AvaloniaMemberAttributeProvider.cs" /> <Compile Include="PortableXaml\AvaloniaMemberAttributeProvider.cs" />

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

@ -4,19 +4,19 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization; 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.Markup.Xaml.Templates;
using Avalonia.Styling; using Avalonia.Styling;
using Portable.Xaml; using Avalonia.Utilities;
using Portable.Xaml.ComponentModel; using Portable.Xaml.ComponentModel;
using Portable.Xaml.Markup;
namespace Avalonia.Markup.Xaml.Converters namespace Avalonia.Markup.Xaml.Converters
{ {
public class AvaloniaPropertyTypeConverter : TypeConverter public class AvaloniaPropertyTypeConverter : TypeConverter
{ {
private static readonly Regex regex = new Regex(@"^\(?(\w*)\.(\w*)\)?|(.*)$");
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{ {
return sourceType == typeof(string); return sourceType == typeof(string);
@ -24,39 +24,47 @@ namespace Avalonia.Markup.Xaml.Converters
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{ {
var (owner, propertyName) = ParseProperty((string)value); var registry = AvaloniaPropertyRegistry.Instance;
var ownerType = TryResolveOwnerByName(context, owner) ?? var parser = new PropertyParser();
context.GetFirstAmbientValue<ControlTemplate>()?.TargetType ?? var (ns, owner, propertyName) = parser.Parse(new CharacterReader(((string)value).AsSpan()));
context.GetFirstAmbientValue<Style>()?.Selector?.TargetType; 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( throw new XamlLoadException($"Could not find property '{effectiveOwner.Name}.{propertyName}'.");
$"Could not determine the owner type for property '{propertyName}'. " +
"Please fully qualify the property name or specify a target type on " +
"the containing template.");
} }
var property = AvaloniaPropertyRegistry.Instance.FindRegistered(ownerType, propertyName); if (effectiveOwner != targetType &&
!property.IsAttached &&
if (property == null) !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; return property;
} }
private Type TryResolveOwnerByName(ITypeDescriptorContext context, string owner) private Type TryResolveOwnerByName(ITypeDescriptorContext context, string ns, string owner)
{ {
if (owner != null) if (owner != null)
{ {
var resolver = context.GetService<IXamlTypeResolver>(); var result = context.ResolveType(ns, owner);
var result = resolver.Resolve(owner);
if (result == null) 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; return result;
@ -64,19 +72,5 @@ namespace Avalonia.Markup.Xaml.Converters
return null; 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);
}
}
} }
} }

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

@ -32,176 +32,29 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
{ {
var descriptorContext = (ITypeDescriptorContext)serviceProvider; var descriptorContext = (ITypeDescriptorContext)serviceProvider;
var pathInfo = ParsePath(Path, descriptorContext);
ValidateState(pathInfo);
return new Binding return new Binding
{ {
TypeResolver = descriptorContext.ResolveType, TypeResolver = descriptorContext.ResolveType,
Converter = Converter, Converter = Converter,
ConverterParameter = ConverterParameter, ConverterParameter = ConverterParameter,
ElementName = pathInfo.ElementName ?? ElementName, ElementName = ElementName,
FallbackValue = FallbackValue, FallbackValue = FallbackValue,
Mode = Mode, Mode = Mode,
Path = pathInfo.Path, Path = Path,
Priority = Priority, Priority = Priority,
Source = Source, Source = Source,
RelativeSource = pathInfo.RelativeSource ?? RelativeSource, StringFormat = StringFormat,
DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)serviceProvider)) RelativeSource = RelativeSource,
DefaultAnchor = new WeakReference(GetDefaultAnchor(descriptorContext))
}; };
} }
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) private static object GetDefaultAnchor(ITypeDescriptorContext context)
{ {
object anchor = null; // If the target is not a control, so we need to find an anchor that will let us look
// 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 // up named controls and style resources. First look for the closest IControl in
// the context. // 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 // 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. // file could be a XAML file containing only styles.
@ -227,6 +80,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
public object Source { get; set; } public object Source { get; set; }
public string StringFormat { get; set; }
public RelativeSource RelativeSource { get; set; } 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 = r.ParseIdentifier();
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.ToString() :
throw new ExpressionParseException(r.Position, "Unexpected ':'.");
}
else if (!r.End && r.TakeIf('.'))
{
owner = owner == null ?
token.ToString() :
throw new ExpressionParseException(r.Position, "Unexpected '.'.");
}
else
{
name = token.ToString();
}
} 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 System.Reflection;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Markup.Data;
using Avalonia.Markup.Xaml.Templates; using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Metadata; using Avalonia.Metadata;
@ -33,6 +32,7 @@ namespace Avalonia.Markup.Xaml.Context
private static readonly IEnumerable<Assembly> ForcedAssemblies = new[] private static readonly IEnumerable<Assembly> ForcedAssemblies = new[]
{ {
typeof(AvaloniaObject).GetTypeInfo().Assembly, typeof(AvaloniaObject).GetTypeInfo().Assembly,
typeof(Animation.Animation).GetTypeInfo().Assembly,
typeof(Control).GetTypeInfo().Assembly, typeof(Control).GetTypeInfo().Assembly,
typeof(Style).GetTypeInfo().Assembly, typeof(Style).GetTypeInfo().Assembly,
typeof(DataTemplate).GetTypeInfo().Assembly, typeof(DataTemplate).GetTypeInfo().Assembly,
@ -146,4 +146,4 @@ namespace Avalonia.Markup.Xaml.Context
return null; return null;
} }
} }
} }

2
src/Markup/Avalonia.Markup/Avalonia.Markup.csproj

@ -10,4 +10,4 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\build\Rx.props" /> <Import Project="..\..\..\build\Rx.props" />
<Import Project="..\..\..\build\System.Memory.props" /> <Import Project="..\..\..\build\System.Memory.props" />
</Project> </Project>

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

@ -84,6 +84,11 @@ namespace Avalonia.Data
/// </summary> /// </summary>
public object Source { get; set; } public object Source { get; set; }
/// <summary>
/// Gets or sets the string format.
/// </summary>
public string StringFormat { get; set; }
public WeakReference DefaultAnchor { get; set; } public WeakReference DefaultAnchor { get; set; }
/// <summary> /// <summary>
@ -105,34 +110,53 @@ namespace Avalonia.Data
ExpressionObserver observer; ExpressionObserver observer;
var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver);
if (ElementName != null) if (ElementName != null)
{ {
observer = CreateElementObserver( observer = CreateElementObserver(
(target as IStyledElement) ?? (anchor as IStyledElement), (target as IStyledElement) ?? (anchor as IStyledElement),
ElementName, ElementName,
Path, node);
enableDataValidation);
} }
else if (Source != null) else if (Source != null)
{ {
observer = CreateSourceObserver(Source, Path, enableDataValidation); observer = CreateSourceObserver(Source, node);
} }
else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext) 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.Mode == RelativeSourceMode.DataContext)
{ {
observer = CreateDataContextObserver( observer = CreateDataContextObserver(
target, target,
Path, node,
targetProperty == StyledElement.DataContextProperty, targetProperty == StyledElement.DataContextProperty,
anchor, anchor);
enableDataValidation);
} }
else if (RelativeSource.Mode == RelativeSourceMode.Self) else if (RelativeSource.Mode == RelativeSourceMode.Self)
{ {
observer = CreateSourceObserver(target, Path, enableDataValidation); observer = CreateSourceObserver(target, node);
} }
else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) 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) else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor)
{ {
@ -144,8 +168,7 @@ namespace Avalonia.Data
observer = CreateFindAncestorObserver( observer = CreateFindAncestorObserver(
(target as IStyledElement) ?? (anchor as IStyledElement), (target as IStyledElement) ?? (anchor as IStyledElement),
RelativeSource, RelativeSource,
Path, node);
enableDataValidation);
} }
else else
{ {
@ -163,11 +186,23 @@ namespace Avalonia.Data
fallback = null; fallback = null;
} }
var converter = Converter;
var targetType = targetProperty?.PropertyType ?? typeof(object);
// We only respect `StringFormat` if the type of the property we're assigning to will
// accept a string. Note that this is slightly different to WPF in that WPF only applies
// `StringFormat` for target type `string` (not `object`).
if (!string.IsNullOrWhiteSpace(StringFormat) &&
(targetType == typeof(string) || targetType == typeof(object)))
{
converter = new StringFormatValueConverter(StringFormat, converter);
}
var subject = new BindingExpression( var subject = new BindingExpression(
observer, observer,
targetProperty?.PropertyType ?? typeof(object), targetType,
fallback, fallback,
Converter ?? DefaultValueConverter.Instance, converter ?? DefaultValueConverter.Instance,
ConverterParameter, ConverterParameter,
Priority); Priority);
@ -176,10 +211,9 @@ namespace Avalonia.Data
private ExpressionObserver CreateDataContextObserver( private ExpressionObserver CreateDataContextObserver(
IAvaloniaObject target, IAvaloniaObject target,
string path, ExpressionNode node,
bool targetIsDataContext, bool targetIsDataContext,
object anchor, object anchor)
bool enableDataValidation)
{ {
Contract.Requires<ArgumentNullException>(target != null); Contract.Requires<ArgumentNullException>(target != null);
@ -195,48 +229,41 @@ namespace Avalonia.Data
if (!targetIsDataContext) if (!targetIsDataContext)
{ {
var result = ExpressionObserverBuilder.Build( var result = new ExpressionObserver(
() => target.GetValue(StyledElement.DataContextProperty), () => target.GetValue(StyledElement.DataContextProperty),
path, node,
new UpdateSignal(target, StyledElement.DataContextProperty), new UpdateSignal(target, StyledElement.DataContextProperty),
enableDataValidation, null);
typeResolver: TypeResolver);
return result; return result;
} }
else else
{ {
return ExpressionObserverBuilder.Build( return new ExpressionObserver(
GetParentDataContext(target), GetParentDataContext(target),
path, node,
enableDataValidation, null);
typeResolver: TypeResolver);
} }
} }
private ExpressionObserver CreateElementObserver( private ExpressionObserver CreateElementObserver(
IStyledElement target, IStyledElement target,
string elementName, string elementName,
string path, ExpressionNode node)
bool enableDataValidation)
{ {
Contract.Requires<ArgumentNullException>(target != null); Contract.Requires<ArgumentNullException>(target != null);
var description = $"#{elementName}.{path}"; var result = new ExpressionObserver(
var result = ExpressionObserverBuilder.Build(
ControlLocator.Track(target, elementName), ControlLocator.Track(target, elementName),
path, node,
enableDataValidation, null);
description,
typeResolver: TypeResolver);
return result; return result;
} }
private ExpressionObserver CreateFindAncestorObserver( private ExpressionObserver CreateFindAncestorObserver(
IStyledElement target, IStyledElement target,
RelativeSource relativeSource, RelativeSource relativeSource,
string path, ExpressionNode node)
bool enableDataValidation)
{ {
Contract.Requires<ArgumentNullException>(target != null); Contract.Requires<ArgumentNullException>(target != null);
@ -260,36 +287,32 @@ namespace Avalonia.Data
throw new InvalidOperationException("Invalid tree to traverse."); throw new InvalidOperationException("Invalid tree to traverse.");
} }
return ExpressionObserverBuilder.Build( return new ExpressionObserver(
controlLocator, controlLocator,
path, node,
enableDataValidation, null);
typeResolver: TypeResolver);
} }
private ExpressionObserver CreateSourceObserver( private ExpressionObserver CreateSourceObserver(
object source, object source,
string path, ExpressionNode node)
bool enableDataValidation)
{ {
Contract.Requires<ArgumentNullException>(source != null); Contract.Requires<ArgumentNullException>(source != null);
return ExpressionObserverBuilder.Build(source, path, enableDataValidation, typeResolver: TypeResolver); return new ExpressionObserver(source, node);
} }
private ExpressionObserver CreateTemplatedParentObserver( private ExpressionObserver CreateTemplatedParentObserver(
IAvaloniaObject target, IAvaloniaObject target,
string path, ExpressionNode node)
bool enableDataValidation)
{ {
Contract.Requires<ArgumentNullException>(target != null); Contract.Requires<ArgumentNullException>(target != null);
var result = ExpressionObserverBuilder.Build( var result = new ExpressionObserver(
() => target.GetValue(StyledElement.TemplatedParentProperty), () => target.GetValue(StyledElement.TemplatedParentProperty),
path, node,
new UpdateSignal(target, StyledElement.TemplatedParentProperty), new UpdateSignal(target, StyledElement.TemplatedParentProperty),
enableDataValidation, null);
typeResolver: TypeResolver);
return result; return result;
} }

6
src/Markup/Avalonia.Markup/Data/TemplateBinding.cs

@ -100,7 +100,9 @@ namespace Avalonia.Data
CultureInfo.CurrentCulture); CultureInfo.CurrentCulture);
} }
_target.TemplatedParent.SetValue(Property, value, BindingPriority.TemplatedParent); // Use LocalValue priority here, as TemplatedParent doesn't make sense on controls
// that aren't template children.
_target.TemplatedParent.SetValue(Property, value, BindingPriority.LocalValue);
} }
} }
@ -171,7 +173,7 @@ namespace Avalonia.Data
{ {
if (e.Property == Property) if (e.Property == Property)
{ {
PublishNext(_target.TemplatedParent.GetValue(Property)); PublishValue();
} }
} }
} }

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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Utilities;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
@ -10,7 +11,7 @@ namespace Avalonia.Markup.Parsers
{ {
internal static class ArgumentListParser internal static class ArgumentListParser
{ {
public static IList<string> ParseArguments(this ref Reader r, char open, char close) public static IList<string> ParseArguments(this ref CharacterReader r, char open, char close, char delimiter = ',')
{ {
if (r.Peek == open) if (r.Peek == open)
{ {
@ -20,7 +21,7 @@ namespace Avalonia.Markup.Parsers
while (!r.End) while (!r.End)
{ {
var argument = r.TakeWhile(c => c != ',' && c != close); var argument = r.TakeWhile(c => c != delimiter && c != close && !char.IsWhiteSpace(c));
if (argument.IsEmpty) if (argument.IsEmpty)
{ {
throw new ExpressionParseException(r.Position, "Expected indexer argument."); throw new ExpressionParseException(r.Position, "Expected indexer argument.");
@ -32,7 +33,7 @@ namespace Avalonia.Markup.Parsers
if (r.End) if (r.End)
{ {
throw new ExpressionParseException(r.Position, "Expected ','."); throw new ExpressionParseException(r.Position, $"Expected '{delimiter}'.");
} }
else if (r.TakeIf(close)) else if (r.TakeIf(close))
{ {
@ -40,9 +41,9 @@ namespace Avalonia.Markup.Parsers
} }
else else
{ {
if (r.Take() != ',') if (r.Take() != delimiter)
{ {
throw new ExpressionParseException(r.Position, "Expected ','."); throw new ExpressionParseException(r.Position, $"Expected '{delimiter}'.");
} }
r.SkipWhitespace(); r.SkipWhitespace();

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

Loading…
Cancel
Save