Browse Source

Merge branch 'master' into master

pull/3589/head
Vadim Melnikov 6 years ago
committed by GitHub
parent
commit
15d90f59b6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      readme.md
  2. 8
      samples/ControlCatalog/Pages/ProgressBarPage.xaml
  3. 9
      src/Avalonia.Base/AvaloniaObject.cs
  4. 39
      src/Avalonia.Base/AvaloniaProperty.cs
  5. 45
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  6. 15
      src/Avalonia.Base/DirectPropertyBase.cs
  7. 4
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  8. 5
      src/Avalonia.Base/PropertyStore/IValueSink.cs
  9. 5
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  10. 15
      src/Avalonia.Base/StyledPropertyBase.cs
  11. 9
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  12. 9
      src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs
  13. 9
      src/Avalonia.Base/ValueStore.cs
  14. 13
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  15. 16
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  16. 2
      src/Avalonia.Controls/AutoCompleteBox.cs
  17. 25
      src/Avalonia.Controls/Button.cs
  18. 28
      src/Avalonia.Controls/ButtonSpinner.cs
  19. 2
      src/Avalonia.Controls/Calendar/DatePicker.cs
  20. 4
      src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs
  21. 20
      src/Avalonia.Controls/ControlExtensions.cs
  22. 41
      src/Avalonia.Controls/Expander.cs
  23. 44
      src/Avalonia.Controls/ItemsControl.cs
  24. 30
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  25. 283
      src/Avalonia.Controls/Primitives/Popup.cs
  26. 24
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  27. 8
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  28. 24
      src/Avalonia.Controls/Primitives/ToggleButton.cs
  29. 28
      src/Avalonia.Controls/Primitives/Track.cs
  30. 53
      src/Avalonia.Controls/ProgressBar.cs
  31. 4
      src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs
  32. 16
      src/Avalonia.Controls/ScrollViewer.cs
  33. 6
      src/Avalonia.Controls/SelectionChangedEventArgs.cs
  34. 24
      src/Avalonia.Controls/Slider.cs
  35. 42
      src/Avalonia.Controls/TextBox.cs
  36. 4
      src/Avalonia.Controls/TreeView.cs
  37. 4
      src/Avalonia.Controls/Window.cs
  38. 2
      src/Avalonia.Input/IInputElement.cs
  39. 42
      src/Avalonia.Input/InputElement.cs
  40. 12
      src/Avalonia.Layout/LayoutManager.cs
  41. 11
      src/Avalonia.Native/WindowImplBase.cs
  42. 28
      src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs
  43. 104
      src/Avalonia.Styling/StyledElement.cs
  44. 4
      src/Avalonia.Styling/Styling/Setter.cs
  45. 35
      src/Avalonia.Styling/Styling/Style.cs
  46. 4
      src/Avalonia.Styling/Styling/Styles.cs
  47. 21
      src/Avalonia.Themes.Default/ComboBox.xaml
  48. 86
      src/Avalonia.Themes.Default/ProgressBar.xaml
  49. 4
      src/Avalonia.Themes.Default/TextBox.xaml
  50. 5
      src/Avalonia.Visuals/Media/ArcSegment.cs
  51. 3
      src/Avalonia.Visuals/Media/BezierSegment .cs
  52. 3
      src/Avalonia.Visuals/Media/LineSegment.cs
  53. 3
      src/Avalonia.Visuals/Media/PathFigure.cs
  54. 4
      src/Avalonia.Visuals/Media/PathGeometry.cs
  55. 3
      src/Avalonia.Visuals/Media/QuadraticBezierSegment .cs
  56. 26
      src/Avalonia.Visuals/Visual.cs
  57. 17
      src/Avalonia.X11/X11Window.cs
  58. 2
      src/Windows/Avalonia.Win32/WindowImpl.cs
  59. 17
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs
  60. 105
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  61. 17
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs
  62. 12
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs
  63. 23
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs
  64. 12
      tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs
  65. 22
      tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs
  66. 2
      tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs
  67. 46
      tests/Avalonia.Benchmarks/Layout/CalendarBenchmark.cs
  68. 36
      tests/Avalonia.Benchmarks/NullFormattedTextImpl.cs
  69. 78
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  70. 28
      tests/Avalonia.Benchmarks/NullThreadingPlatform.cs
  71. 47
      tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs
  72. 46
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  73. 31
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  74. 32
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  75. 28
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  76. 22
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  77. 24
      tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
  78. 51
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  79. 33
      tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs
  80. 52
      tests/Avalonia.LeakTests/ControlTests.cs
  81. 42
      tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs

2
readme.md

@ -24,6 +24,8 @@ Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?it
For those without Visual Studio, a starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core). For those without Visual Studio, a starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core).
If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature, so only **YOU** can make it happen.
Avalonia is delivered via <b>NuGet</b> package manager. You can find the packages here: [stable(ish)](https://www.nuget.org/packages/Avalonia/) Avalonia is delivered via <b>NuGet</b> package manager. You can find the packages here: [stable(ish)](https://www.nuget.org/packages/Avalonia/)
Use these commands in the Package Manager console to install Avalonia manually: Use these commands in the Package Manager console to install Avalonia manually:

8
samples/ControlCatalog/Pages/ProgressBarPage.xaml

@ -6,15 +6,19 @@
<TextBlock Classes="h2">A progress bar control</TextBlock> <TextBlock Classes="h2">A progress bar control</TextBlock>
<StackPanel> <StackPanel>
<CheckBox
x:Name="showProgress"
Margin="10,16,0,0"
Content="Show Progress Text" />
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Spacing="16"> Spacing="16">
<StackPanel Spacing="16"> <StackPanel Spacing="16">
<ProgressBar Value="{Binding #hprogress.Value}" /> <ProgressBar ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #hprogress.Value}" />
<ProgressBar IsIndeterminate="True"/> <ProgressBar IsIndeterminate="True"/>
</StackPanel> </StackPanel>
<ProgressBar Value="{Binding #vprogress.Value}" Orientation="Vertical" /> <ProgressBar ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #vprogress.Value}" Orientation="Vertical" />
<ProgressBar Orientation="Vertical" IsIndeterminate="True" /> <ProgressBar Orientation="Vertical" IsIndeterminate="True" />
</StackPanel> </StackPanel>
<StackPanel Margin="16"> <StackPanel Margin="16">

9
src/Avalonia.Base/AvaloniaObject.cs

@ -34,7 +34,6 @@ namespace Avalonia
public AvaloniaObject() public AvaloniaObject()
{ {
VerifyAccess(); VerifyAccess();
AvaloniaPropertyRegistry.Instance.NotifyInitialized(this);
} }
/// <summary> /// <summary>
@ -479,7 +478,13 @@ namespace Avalonia
} }
} }
void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) { } void IValueSink.Completed<T>(
StyledPropertyBase<T> property,
IPriorityValueEntry entry,
Optional<T> oldValue)
{
((IValueSink)this).ValueChanged(property, BindingPriority.Unset, oldValue, default);
}
/// <summary> /// <summary>
/// Called for each inherited property when the <see cref="InheritanceParent"/> changes. /// Called for each inherited property when the <see cref="InheritanceParent"/> changes.

39
src/Avalonia.Base/AvaloniaProperty.cs

@ -20,7 +20,6 @@ namespace Avalonia
public static readonly object UnsetValue = new UnsetValueType(); public static readonly object UnsetValue = new UnsetValueType();
private static int s_nextId; private static int s_nextId;
private readonly Subject<AvaloniaPropertyChangedEventArgs> _initialized;
private readonly Subject<AvaloniaPropertyChangedEventArgs> _changed; private readonly Subject<AvaloniaPropertyChangedEventArgs> _changed;
private readonly PropertyMetadata _defaultMetadata; private readonly PropertyMetadata _defaultMetadata;
private readonly Dictionary<Type, PropertyMetadata> _metadata; private readonly Dictionary<Type, PropertyMetadata> _metadata;
@ -53,7 +52,6 @@ namespace Avalonia
throw new ArgumentException("'name' may not contain periods."); throw new ArgumentException("'name' may not contain periods.");
} }
_initialized = new Subject<AvaloniaPropertyChangedEventArgs>();
_changed = new Subject<AvaloniaPropertyChangedEventArgs>(); _changed = new Subject<AvaloniaPropertyChangedEventArgs>();
_metadata = new Dictionary<Type, PropertyMetadata>(); _metadata = new Dictionary<Type, PropertyMetadata>();
@ -81,7 +79,6 @@ namespace Avalonia
Contract.Requires<ArgumentNullException>(source != null); Contract.Requires<ArgumentNullException>(source != null);
Contract.Requires<ArgumentNullException>(ownerType != null); Contract.Requires<ArgumentNullException>(ownerType != null);
_initialized = source._initialized;
_changed = source._changed; _changed = source._changed;
_metadata = new Dictionary<Type, PropertyMetadata>(); _metadata = new Dictionary<Type, PropertyMetadata>();
@ -136,22 +133,6 @@ namespace Avalonia
/// </summary> /// </summary>
public virtual bool IsReadOnly => false; public virtual bool IsReadOnly => false;
/// <summary>
/// Gets an observable that is fired when this property is initialized on a
/// new <see cref="AvaloniaObject"/> instance.
/// </summary>
/// <remarks>
/// This observable is fired each time a new <see cref="AvaloniaObject"/> is constructed
/// for all properties registered on the object's type. The default value of the property
/// for the object is passed in the args' NewValue (OldValue will always be
/// <see cref="UnsetValue"/>.
/// </remarks>
/// <value>
/// An observable that is fired when this property is initialized on a new
/// <see cref="AvaloniaObject"/> instance.
/// </value>
public IObservable<AvaloniaPropertyChangedEventArgs> Initialized => _initialized;
/// <summary> /// <summary>
/// Gets an observable that is fired when this property changes on any /// Gets an observable that is fired when this property changes on any
/// <see cref="AvaloniaObject"/> instance. /// <see cref="AvaloniaObject"/> instance.
@ -488,26 +469,6 @@ namespace Avalonia
return Name; return Name;
} }
/// <summary>
/// True if <see cref="Initialized"/> has any observers.
/// </summary>
internal bool HasNotifyInitializedObservers => _initialized.HasObservers;
/// <summary>
/// Notifies the <see cref="Initialized"/> observable.
/// </summary>
/// <param name="o">The object being initialized.</param>
internal abstract void NotifyInitialized(IAvaloniaObject o);
/// <summary>
/// Notifies the <see cref="Initialized"/> observable.
/// </summary>
/// <param name="e">The observable arguments.</param>
internal void NotifyInitialized(AvaloniaPropertyChangedEventArgs e)
{
_initialized.OnNext(e);
}
/// <summary> /// <summary>
/// Notifies the <see cref="Changed"/> observable. /// Notifies the <see cref="Changed"/> observable.
/// </summary> /// </summary>

45
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -415,51 +415,6 @@ namespace Avalonia
_inheritedCache.Clear(); _inheritedCache.Clear();
} }
internal void NotifyInitialized(AvaloniaObject o)
{
Contract.Requires<ArgumentNullException>(o != null);
var type = o.GetType();
if (!_initializedCache.TryGetValue(type, out var initializationData))
{
var visited = new HashSet<AvaloniaProperty>();
initializationData = new List<PropertyInitializationData>();
foreach (AvaloniaProperty property in GetRegistered(type))
{
if (property.IsDirect)
{
initializationData.Add(new PropertyInitializationData(property, (IDirectPropertyAccessor)property));
}
else
{
initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type));
}
visited.Add(property);
}
foreach (AvaloniaProperty property in GetRegisteredAttached(type))
{
if (!visited.Contains(property))
{
initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type));
visited.Add(property);
}
}
_initializedCache.Add(type, initializationData);
}
foreach (PropertyInitializationData data in initializationData)
{
data.Property.NotifyInitialized(o);
}
}
private readonly struct PropertyInitializationData private readonly struct PropertyInitializationData
{ {
public AvaloniaProperty Property { get; } public AvaloniaProperty Property { get; }

15
src/Avalonia.Base/DirectPropertyBase.cs

@ -101,21 +101,6 @@ namespace Avalonia
return (DirectPropertyMetadata<TValue>)base.GetMetadata(type); return (DirectPropertyMetadata<TValue>)base.GetMetadata(type);
} }
/// <inheritdoc/>
internal override void NotifyInitialized(IAvaloniaObject o)
{
if (HasNotifyInitializedObservers)
{
var e = new AvaloniaPropertyChangedEventArgs<TValue>(
o,
this,
default,
InvokeGetter(o),
BindingPriority.Unset);
NotifyInitialized(e);
}
}
/// <inheritdoc/> /// <inheritdoc/>
internal override void RouteClearValue(IAvaloniaObject o) internal override void RouteClearValue(IAvaloniaObject o)
{ {

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

@ -48,10 +48,10 @@ namespace Avalonia.PropertyStore
{ {
_subscription?.Dispose(); _subscription?.Dispose();
_subscription = null; _subscription = null;
_sink.Completed(Property, this); _sink.Completed(Property, this, Value);
} }
public void OnCompleted() => _sink.Completed(Property, this); public void OnCompleted() => _sink.Completed(Property, this, Value);
public void OnError(Exception error) public void OnError(Exception error)
{ {

5
src/Avalonia.Base/PropertyStore/IValueSink.cs

@ -15,6 +15,9 @@ namespace Avalonia.PropertyStore
Optional<T> oldValue, Optional<T> oldValue,
BindingValue<T> newValue); BindingValue<T> newValue);
void Completed(AvaloniaProperty property, IPriorityValueEntry entry); void Completed<T>(
StyledPropertyBase<T> property,
IPriorityValueEntry entry,
Optional<T> oldValue);
} }
} }

5
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@ -117,7 +117,10 @@ namespace Avalonia.PropertyStore
UpdateEffectiveValue(); UpdateEffectiveValue();
} }
void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) void IValueSink.Completed<TValue>(
StyledPropertyBase<TValue> property,
IPriorityValueEntry entry,
Optional<TValue> oldValue)
{ {
_entries.Remove((IPriorityValueEntry<T>)entry); _entries.Remove((IPriorityValueEntry<T>)entry);
UpdateEffectiveValue(); UpdateEffectiveValue();

15
src/Avalonia.Base/StyledPropertyBase.cs

@ -181,21 +181,6 @@ namespace Avalonia
/// <inheritdoc/> /// <inheritdoc/>
object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type);
/// <inheritdoc/>
internal override void NotifyInitialized(IAvaloniaObject o)
{
if (HasNotifyInitializedObservers)
{
var e = new AvaloniaPropertyChangedEventArgs<TValue>(
o,
this,
default,
o.GetValue(this),
BindingPriority.Unset);
NotifyInitialized(e);
}
}
/// <inheritdoc/> /// <inheritdoc/>
internal override void RouteClearValue(IAvaloniaObject o) internal override void RouteClearValue(IAvaloniaObject o)
{ {

9
src/Avalonia.Base/Utilities/TypeUtilities.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.ComponentModel;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@ -185,6 +186,14 @@ namespace Avalonia.Utilities
} }
} }
var typeConverter = TypeDescriptor.GetConverter(to);
if (typeConverter.CanConvertFrom(from) == true)
{
result = typeConverter.ConvertFrom(null, culture, value);
return true;
}
var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit | OperatorType.Explicit); var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit | OperatorType.Explicit);
if (cast != null) if (cast != null)

9
src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs

@ -183,11 +183,16 @@ namespace Avalonia.Utilities
for (int c = 0; c < _count; c++) for (int c = 0; c < _count; c++)
{ {
var r = _data[c]; var r = _data[c];
TSubscriber target = null;
r.Subscriber?.TryGetTarget(out target);
//Mark current index as first empty //Mark current index as first empty
if (r.Subscriber == null && empty == -1) if (target == null && empty == -1)
empty = c; empty = c;
//If current element isn't null and we have an empty one //If current element isn't null and we have an empty one
if (r.Subscriber != null && empty != -1) if (target != null && empty != -1)
{ {
_data[c] = default; _data[c] = default;
_data[empty] = r; _data[empty] = r;

9
src/Avalonia.Base/ValueStore.cs

@ -148,7 +148,7 @@ namespace Avalonia
_values.Remove(property); _values.Remove(property);
_sink.ValueChanged( _sink.ValueChanged(
property, property,
BindingPriority.LocalValue, BindingPriority.Unset,
old, old,
BindingValue<T>.Unset); BindingValue<T>.Unset);
} }
@ -190,13 +190,17 @@ namespace Avalonia
_sink.ValueChanged(property, priority, oldValue, newValue); _sink.ValueChanged(property, priority, oldValue, newValue);
} }
void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) void IValueSink.Completed<T>(
StyledPropertyBase<T> property,
IPriorityValueEntry entry,
Optional<T> oldValue)
{ {
if (_values.TryGetValue(property, out var slot)) if (_values.TryGetValue(property, out var slot))
{ {
if (slot == entry) if (slot == entry)
{ {
_values.Remove(property); _values.Remove(property);
_sink.Completed(property, entry, oldValue);
} }
} }
} }
@ -228,6 +232,7 @@ namespace Avalonia
else else
{ {
var priorityValue = new PriorityValue<T>(_owner, property, this, l); var priorityValue = new PriorityValue<T>(_owner, property, this, l);
priorityValue.SetValue(value, priority);
_values.SetValue(property, priorityValue); _values.SetValue(property, priorityValue);
} }
} }

13
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -149,6 +149,9 @@ namespace Avalonia.Controls
private IEnumerable _items; private IEnumerable _items;
public event EventHandler<ScrollEventArgs> HorizontalScroll;
public event EventHandler<ScrollEventArgs> VerticalScroll;
/// <summary> /// <summary>
/// Identifies the CanUserReorderColumns dependency property. /// Identifies the CanUserReorderColumns dependency property.
/// </summary> /// </summary>
@ -373,7 +376,11 @@ namespace Avalonia.Controls
public bool IsValid public bool IsValid
{ {
get { return _isValid; } get { return _isValid; }
internal set { SetAndRaise(IsValidProperty, ref _isValid, value); } internal set
{
SetAndRaise(IsValidProperty, ref _isValid, value);
PseudoClasses.Set(":invalid", !value);
}
} }
public static readonly StyledProperty<double> MaxColumnWidthProperty = public static readonly StyledProperty<double> MaxColumnWidthProperty =
@ -656,8 +663,6 @@ namespace Avalonia.Controls
HorizontalScrollBarVisibilityProperty, HorizontalScrollBarVisibilityProperty,
VerticalScrollBarVisibilityProperty); VerticalScrollBarVisibilityProperty);
PseudoClass<DataGrid, bool>(IsValidProperty, x => !x, ":invalid");
ItemsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnItemsPropertyChanged(e)); ItemsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnItemsPropertyChanged(e));
CanUserResizeColumnsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnCanUserResizeColumnsChanged(e)); CanUserResizeColumnsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnCanUserResizeColumnsChanged(e));
ColumnWidthProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnColumnWidthChanged(e)); ColumnWidthProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnColumnWidthChanged(e));
@ -4223,6 +4228,7 @@ namespace Avalonia.Controls
private void HorizontalScrollBar_Scroll(object sender, ScrollEventArgs e) private void HorizontalScrollBar_Scroll(object sender, ScrollEventArgs e)
{ {
ProcessHorizontalScroll(e.ScrollEventType); ProcessHorizontalScroll(e.ScrollEventType);
HorizontalScroll?.Invoke(sender, e);
} }
private bool IsColumnOutOfBounds(int columnIndex) private bool IsColumnOutOfBounds(int columnIndex)
@ -5555,6 +5561,7 @@ namespace Avalonia.Controls
private void VerticalScrollBar_Scroll(object sender, ScrollEventArgs e) private void VerticalScrollBar_Scroll(object sender, ScrollEventArgs e)
{ {
ProcessVerticalScroll(e.ScrollEventType); ProcessVerticalScroll(e.ScrollEventType);
VerticalScroll?.Invoke(sender, e);
} }
//TODO: Ensure left button is checked for //TODO: Ensure left button is checked for

16
src/Avalonia.Controls.DataGrid/Themes/Default.xaml

@ -202,24 +202,24 @@
<Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}" /> <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}" />
<Setter Property="DropLocationIndicatorTemplate"> <Setter Property="DropLocationIndicatorTemplate">
<Template> <Template>
<Rectangle Fill="#FF3F4346" Width="2"/> <Rectangle Fill="{DynamicResource ThemeBorderHighColor}" Width="2"/>
</Template> </Template>
</Setter> </Setter>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Border BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}"> <Border Background="{TemplateBinding Background}"
<Grid BorderThickness="{TemplateBinding BorderThickness}"
RowDefinitions="Auto,*,Auto,Auto" BorderBrush="{TemplateBinding BorderBrush}">
ColumnDefinitions="Auto,*,Auto"> <Grid RowDefinitions="Auto,*,Auto,Auto" ColumnDefinitions="Auto,*,Auto">
<DataGridColumnHeader Name="PART_TopLeftCornerHeader" Width="22" /> <DataGridColumnHeader Name="PART_TopLeftCornerHeader" Width="22" />
<DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter" Grid.Column="1"/> <DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter" Grid.Column="1"/>
<DataGridColumnHeader Name="PART_TopRightCornerHeader" Grid.Column="2"/> <DataGridColumnHeader Name="PART_TopRightCornerHeader" Grid.Column="2"/>
<Rectangle Name="PART_ColumnHeadersAndRowsSeparator" Grid.ColumnSpan="3" VerticalAlignment="Bottom" StrokeThickness="1" Height="1" Fill="#FFC9CACA"/> <Rectangle Name="PART_ColumnHeadersAndRowsSeparator" Grid.ColumnSpan="3" VerticalAlignment="Bottom" StrokeThickness="1" Height="1" Fill="{DynamicResource ThemeControlMidHighBrush}"/>
<DataGridRowsPresenter Name="PART_RowsPresenter" Grid.ColumnSpan="2" Grid.Row="1" /> <DataGridRowsPresenter Name="PART_RowsPresenter" Grid.ColumnSpan="2" Grid.Row="1" />
<Rectangle Name="BottomRightCorner" Fill="#FFE9EEF4" Grid.Column="2" Grid.Row="2" /> <Rectangle Name="BottomRightCorner" Fill="{DynamicResource ThemeControlMidHighBrush}" Grid.Column="2" Grid.Row="2" />
<Rectangle Name="BottomLeftCorner" Fill="#FFE9EEF4" Grid.Row="2" Grid.ColumnSpan="2" /> <Rectangle Name="BottomLeftCorner" Fill="{DynamicResource ThemeControlMidHighBrush}" Grid.Row="2" Grid.ColumnSpan="2" />
<ScrollBar Name="PART_VerticalScrollbar" Orientation="Vertical" Grid.Column="2" Grid.Row="1" Width="{DynamicResource ScrollBarThickness}"/> <ScrollBar Name="PART_VerticalScrollbar" Orientation="Vertical" Grid.Column="2" Grid.Row="1" Width="{DynamicResource ScrollBarThickness}"/>
<Grid Grid.Column="1" Grid.Row="2" <Grid Grid.Column="1" Grid.Row="2"

2
src/Avalonia.Controls/AutoCompleteBox.cs

@ -683,7 +683,7 @@ namespace Avalonia.Controls
added.Add(e.NewValue); added.Add(e.NewValue);
} }
OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, added, removed)); OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, removed, added));
} }
/// <summary> /// <summary>

25
src/Avalonia.Controls/Button.cs

@ -91,7 +91,11 @@ namespace Avalonia.Controls
CommandProperty.Changed.Subscribe(CommandChanged); CommandProperty.Changed.Subscribe(CommandChanged);
IsDefaultProperty.Changed.Subscribe(IsDefaultChanged); IsDefaultProperty.Changed.Subscribe(IsDefaultChanged);
IsCancelProperty.Changed.Subscribe(IsCancelChanged); IsCancelProperty.Changed.Subscribe(IsCancelChanged);
PseudoClass<Button>(IsPressedProperty, ":pressed"); }
public Button()
{
UpdatePseudoClasses(IsPressed);
} }
/// <summary> /// <summary>
@ -312,6 +316,20 @@ namespace Avalonia.Controls
IsPressed = false; IsPressed = false;
} }
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
if (property == IsPressedProperty)
{
UpdatePseudoClasses(newValue.GetValueOrDefault<bool>());
}
}
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value) protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{ {
base.UpdateDataValidation(property, value); base.UpdateDataValidation(property, value);
@ -474,5 +492,10 @@ namespace Avalonia.Controls
OnClick(); OnClick();
} }
} }
private void UpdatePseudoClasses(bool isPressed)
{
PseudoClasses.Set(":pressed", isPressed);
}
} }
} }

28
src/Avalonia.Controls/ButtonSpinner.cs

@ -1,5 +1,6 @@
using System; using System;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
@ -34,6 +35,11 @@ namespace Avalonia.Controls
public static readonly StyledProperty<Location> ButtonSpinnerLocationProperty = public static readonly StyledProperty<Location> ButtonSpinnerLocationProperty =
AvaloniaProperty.Register<ButtonSpinner, Location>(nameof(ButtonSpinnerLocation), Location.Right); AvaloniaProperty.Register<ButtonSpinner, Location>(nameof(ButtonSpinnerLocation), Location.Right);
public ButtonSpinner()
{
UpdatePseudoClasses(ButtonSpinnerLocation);
}
private Button _decreaseButton; private Button _decreaseButton;
/// <summary> /// <summary>
/// Gets or sets the DecreaseButton template part. /// Gets or sets the DecreaseButton template part.
@ -85,8 +91,6 @@ namespace Avalonia.Controls
static ButtonSpinner() static ButtonSpinner()
{ {
AllowSpinProperty.Changed.Subscribe(AllowSpinChanged); AllowSpinProperty.Changed.Subscribe(AllowSpinChanged);
PseudoClass<ButtonSpinner, Location>(ButtonSpinnerLocationProperty, location => location == Location.Left, ":left");
PseudoClass<ButtonSpinner, Location>(ButtonSpinnerLocationProperty, location => location == Location.Right, ":right");
} }
/// <summary> /// <summary>
@ -201,6 +205,20 @@ namespace Avalonia.Controls
} }
} }
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
if (property == ButtonSpinnerLocationProperty)
{
UpdatePseudoClasses(newValue.GetValueOrDefault<Location>());
}
}
protected override void OnValidSpinDirectionChanged(ValidSpinDirections oldValue, ValidSpinDirections newValue) protected override void OnValidSpinDirectionChanged(ValidSpinDirections oldValue, ValidSpinDirections newValue)
{ {
SetButtonUsage(); SetButtonUsage();
@ -259,5 +277,11 @@ namespace Avalonia.Controls
OnSpin(new SpinEventArgs(SpinEvent, direction)); OnSpin(new SpinEventArgs(SpinEvent, direction));
} }
} }
private void UpdatePseudoClasses(Location location)
{
PseudoClasses.Set(":left", location == Location.Left);
PseudoClasses.Set(":right", location == Location.Right);
}
} }
} }

2
src/Avalonia.Controls/Calendar/DatePicker.cs

@ -788,7 +788,7 @@ namespace Avalonia.Controls
removedItems.Add(removedDate.Value); removedItems.Add(removedDate.Value);
} }
handler(this, new SelectionChangedEventArgs(SelectingItemsControl.SelectionChangedEvent, addedItems, removedItems)); handler(this, new SelectionChangedEventArgs(SelectingItemsControl.SelectionChangedEvent, removedItems, addedItems));
} }
} }
private void OnCalendarClosed(EventArgs e) private void OnCalendarClosed(EventArgs e)

4
src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs

@ -49,7 +49,7 @@ namespace Avalonia.Controls.Primitives
private void InvokeCollectionChanged(System.Collections.IList removedItems, System.Collections.IList addedItems) private void InvokeCollectionChanged(System.Collections.IList removedItems, System.Collections.IList addedItems)
{ {
_owner.OnSelectedDatesCollectionChanged(new SelectionChangedEventArgs(null, addedItems, removedItems)); _owner.OnSelectedDatesCollectionChanged(new SelectionChangedEventArgs(null, removedItems, addedItems));
} }
/// <summary> /// <summary>
@ -119,7 +119,7 @@ namespace Avalonia.Controls.Primitives
} }
} }
_owner.OnSelectedDatesCollectionChanged(new SelectionChangedEventArgs(null, _addedItems, _owner.RemovedItems)); _owner.OnSelectedDatesCollectionChanged(new SelectionChangedEventArgs(null, _owner.RemovedItems, _addedItems));
_owner.RemovedItems.Clear(); _owner.RemovedItems.Clear();
_owner.UpdateMonths(); _owner.UpdateMonths();
_isRangeAdded = false; _isRangeAdded = false;

20
src/Avalonia.Controls/ControlExtensions.cs

@ -69,26 +69,6 @@ namespace Avalonia.Controls
return nameScope.Find<T>(name); return nameScope.Find<T>(name);
} }
/// <summary>
/// Adds or removes a pseudoclass depending on a boolean value.
/// </summary>
/// <param name="classes">The pseudoclasses collection.</param>
/// <param name="name">The name of the pseudoclass to set.</param>
/// <param name="value">True to add the pseudoclass or false to remove.</param>
public static void Set(this IPseudoClasses classes, string name, bool value)
{
Contract.Requires<ArgumentNullException>(classes != null);
if (value)
{
classes.Add(name);
}
else
{
classes.Remove(name);
}
}
/// <summary> /// <summary>
/// Sets a pseudoclass depending on an observable trigger. /// Sets a pseudoclass depending on an observable trigger.
/// </summary> /// </summary>

41
src/Avalonia.Controls/Expander.cs

@ -1,5 +1,6 @@
using Avalonia.Animation; using Avalonia.Animation;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Data;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -30,16 +31,14 @@ namespace Avalonia.Controls
static Expander() static Expander()
{ {
PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Down, ":down");
PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Up, ":up");
PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Left, ":left");
PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Right, ":right");
PseudoClass<Expander>(IsExpandedProperty, ":expanded");
IsExpandedProperty.Changed.AddClassHandler<Expander>((x, e) => x.OnIsExpandedChanged(e)); IsExpandedProperty.Changed.AddClassHandler<Expander>((x, e) => x.OnIsExpandedChanged(e));
} }
public Expander()
{
UpdatePseudoClasses(ExpandDirection);
}
public IPageTransition ContentTransition public IPageTransition ContentTransition
{ {
get => GetValue(ContentTransitionProperty); get => GetValue(ContentTransitionProperty);
@ -55,7 +54,11 @@ namespace Avalonia.Controls
public bool IsExpanded public bool IsExpanded
{ {
get { return _isExpanded; } get { return _isExpanded; }
set { SetAndRaise(IsExpandedProperty, ref _isExpanded, value); } set
{
SetAndRaise(IsExpandedProperty, ref _isExpanded, value);
PseudoClasses.Set(":expanded", value);
}
} }
protected virtual void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e) protected virtual void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e)
@ -74,5 +77,27 @@ namespace Avalonia.Controls
} }
} }
} }
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
if (property == ExpandDirectionProperty)
{
UpdatePseudoClasses(newValue.GetValueOrDefault<ExpandDirection>());
}
}
private void UpdatePseudoClasses(ExpandDirection d)
{
PseudoClasses.Set(":up", d == ExpandDirection.Up);
PseudoClasses.Set(":down", d == ExpandDirection.Down);
PseudoClasses.Set(":left", d == ExpandDirection.Left);
PseudoClasses.Set(":right", d == ExpandDirection.Right);
}
} }
} }

44
src/Avalonia.Controls/ItemsControl.cs

@ -236,25 +236,7 @@ namespace Avalonia.Controls
// it was added to the Items collection. // it was added to the Items collection.
if (container.ContainerControl != null && container.ContainerControl != container.Item) if (container.ContainerControl != null && container.ContainerControl != container.Item)
{ {
if (ItemContainerGenerator.ContainerType == null) LogicalChildren.Add(container.ContainerControl);
{
var containerControl = container.ContainerControl as ContentPresenter;
if (containerControl != null)
{
((ISetLogicalParent)containerControl).SetParent(this);
containerControl.UpdateChild();
if (containerControl.Child != null)
{
LogicalChildren.Add(containerControl.Child);
}
}
}
else
{
LogicalChildren.Add(container.ContainerControl);
}
} }
} }
} }
@ -272,24 +254,7 @@ namespace Avalonia.Controls
// when it is removed from the Items collection. // when it is removed from the Items collection.
if (container?.ContainerControl != container?.Item) if (container?.ContainerControl != container?.Item)
{ {
if (ItemContainerGenerator.ContainerType == null) LogicalChildren.Remove(container.ContainerControl);
{
var containerControl = container.ContainerControl as ContentPresenter;
if (containerControl != null)
{
((ISetLogicalParent)containerControl).SetParent(null);
if (containerControl.Child != null)
{
LogicalChildren.Remove(containerControl.Child);
}
}
}
else
{
LogicalChildren.Remove(container.ContainerControl);
}
} }
} }
} }
@ -507,7 +472,10 @@ namespace Avalonia.Controls
result = container.GetControl(direction, c, wrap); result = container.GetControl(direction, c, wrap);
from = from ?? result; from = from ?? result;
if (result?.Focusable == true) if (result != null &&
result.Focusable &&
result.IsEffectivelyEnabled &&
result.IsEffectivelyVisible)
{ {
return result; return result;
} }

30
src/Avalonia.Controls/Notifications/WindowNotificationManager.cs

@ -8,6 +8,7 @@ using System.Reactive.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Data;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Controls.Notifications namespace Avalonia.Controls.Notifications
@ -68,15 +69,12 @@ namespace Avalonia.Controls.Notifications
Install(host); Install(host);
}); });
} }
UpdatePseudoClasses(Position);
} }
static WindowNotificationManager() static WindowNotificationManager()
{ {
PseudoClass<WindowNotificationManager, NotificationPosition>(PositionProperty, x => x == NotificationPosition.TopLeft, ":topleft");
PseudoClass<WindowNotificationManager, NotificationPosition>(PositionProperty, x => x == NotificationPosition.TopRight, ":topright");
PseudoClass<WindowNotificationManager, NotificationPosition>(PositionProperty, x => x == NotificationPosition.BottomLeft, ":bottomleft");
PseudoClass<WindowNotificationManager, NotificationPosition>(PositionProperty, x => x == NotificationPosition.BottomRight, ":bottomright");
HorizontalAlignmentProperty.OverrideDefaultValue<WindowNotificationManager>(Layout.HorizontalAlignment.Stretch); HorizontalAlignmentProperty.OverrideDefaultValue<WindowNotificationManager>(Layout.HorizontalAlignment.Stretch);
VerticalAlignmentProperty.OverrideDefaultValue<WindowNotificationManager>(Layout.VerticalAlignment.Stretch); VerticalAlignmentProperty.OverrideDefaultValue<WindowNotificationManager>(Layout.VerticalAlignment.Stretch);
} }
@ -143,6 +141,20 @@ namespace Avalonia.Controls.Notifications
notificationControl.Close(); notificationControl.Close();
} }
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
if (property == PositionProperty)
{
UpdatePseudoClasses(newValue.GetValueOrDefault<NotificationPosition>());
}
}
/// <summary> /// <summary>
/// Installs the <see cref="WindowNotificationManager"/> within the <see cref="AdornerLayer"/> /// Installs the <see cref="WindowNotificationManager"/> within the <see cref="AdornerLayer"/>
/// of the host <see cref="Window"/>. /// of the host <see cref="Window"/>.
@ -155,6 +167,14 @@ namespace Avalonia.Controls.Notifications
adornerLayer?.Children.Add(this); adornerLayer?.Children.Add(this);
} }
private void UpdatePseudoClasses(NotificationPosition position)
{
PseudoClasses.Set(":topleft", position == NotificationPosition.TopLeft);
PseudoClasses.Set(":topright", position == NotificationPosition.TopRight);
PseudoClasses.Set(":bottomleft", position == NotificationPosition.BottomLeft);
PseudoClasses.Set(":bottomright", position == NotificationPosition.BottomRight);
}
public bool HitTest(Point point) => VisualChildren.HitTestCustom(point); public bool HitTest(Point point) => VisualChildren.HitTestCustom(point);
} }
} }

283
src/Avalonia.Controls/Primitives/Popup.cs

@ -2,12 +2,10 @@
// 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.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Interactivity; using Avalonia.Interactivity;
@ -15,6 +13,8 @@ using Avalonia.LogicalTree;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.VisualTree; using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
/// <summary> /// <summary>
@ -25,8 +25,8 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Defines the <see cref="Child"/> property. /// Defines the <see cref="Child"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<Control> ChildProperty = public static readonly StyledProperty<Control?> ChildProperty =
AvaloniaProperty.Register<Popup, Control>(nameof(Child)); AvaloniaProperty.Register<Popup, Control?>(nameof(Child));
/// <summary> /// <summary>
/// Defines the <see cref="IsOpen"/> property. /// Defines the <see cref="IsOpen"/> property.
@ -43,11 +43,13 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<PlacementMode> PlacementModeProperty = public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
AvaloniaProperty.Register<Popup, PlacementMode>(nameof(PlacementMode), defaultValue: PlacementMode.Bottom); AvaloniaProperty.Register<Popup, PlacementMode>(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
#pragma warning disable 618
/// <summary> /// <summary>
/// Defines the <see cref="ObeyScreenEdges"/> property. /// Defines the <see cref="ObeyScreenEdges"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<bool> ObeyScreenEdgesProperty = public static readonly StyledProperty<bool> ObeyScreenEdgesProperty =
AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges), true); AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges), true);
#pragma warning restore 618
/// <summary> /// <summary>
/// Defines the <see cref="HorizontalOffset"/> property. /// Defines the <see cref="HorizontalOffset"/> property.
@ -64,8 +66,8 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Defines the <see cref="PlacementTarget"/> property. /// Defines the <see cref="PlacementTarget"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<Control> PlacementTargetProperty = public static readonly StyledProperty<Control?> PlacementTargetProperty =
AvaloniaProperty.Register<Popup, Control>(nameof(PlacementTarget)); AvaloniaProperty.Register<Popup, Control?>(nameof(PlacementTarget));
/// <summary> /// <summary>
/// Defines the <see cref="StaysOpen"/> property. /// Defines the <see cref="StaysOpen"/> property.
@ -80,12 +82,8 @@ namespace Avalonia.Controls.Primitives
AvaloniaProperty.Register<Popup, bool>(nameof(Topmost)); AvaloniaProperty.Register<Popup, bool>(nameof(Topmost));
private bool _isOpen; private bool _isOpen;
private IPopupHost _popupHost; private bool _ignoreIsOpenChanged;
private TopLevel _topLevel; private PopupOpenState? _openState;
private IDisposable _nonClientListener;
private IDisposable _presenterSubscription;
bool _ignoreIsOpenChanged = false;
private List<IDisposable> _bindings = new List<IDisposable>();
/// <summary> /// <summary>
/// Initializes static members of the <see cref="Popup"/> class. /// Initializes static members of the <see cref="Popup"/> class.
@ -94,31 +92,26 @@ namespace Avalonia.Controls.Primitives
{ {
IsHitTestVisibleProperty.OverrideDefaultValue<Popup>(false); IsHitTestVisibleProperty.OverrideDefaultValue<Popup>(false);
ChildProperty.Changed.AddClassHandler<Popup>((x, e) => x.ChildChanged(e)); ChildProperty.Changed.AddClassHandler<Popup>((x, e) => x.ChildChanged(e));
IsOpenProperty.Changed.AddClassHandler<Popup>((x, e) => x.IsOpenChanged(e)); IsOpenProperty.Changed.AddClassHandler<Popup>((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs<bool>)e));
}
public Popup()
{
} }
/// <summary> /// <summary>
/// Raised when the popup closes. /// Raised when the popup closes.
/// </summary> /// </summary>
public event EventHandler Closed; public event EventHandler? Closed;
/// <summary> /// <summary>
/// Raised when the popup opens. /// Raised when the popup opens.
/// </summary> /// </summary>
public event EventHandler Opened; public event EventHandler? Opened;
public IPopupHost Host => _popupHost; public IPopupHost? Host => _openState?.PopupHost;
/// <summary> /// <summary>
/// Gets or sets the control to display in the popup. /// Gets or sets the control to display in the popup.
/// </summary> /// </summary>
[Content] [Content]
public Control Child public Control? Child
{ {
get { return GetValue(ChildProperty); } get { return GetValue(ChildProperty); }
set { SetValue(ChildProperty, value); } set { SetValue(ChildProperty, value); }
@ -131,7 +124,7 @@ namespace Avalonia.Controls.Primitives
/// This property allows a client to customize the behaviour of the popup by injecting /// This property allows a client to customize the behaviour of the popup by injecting
/// a specialized dependency resolver into the <see cref="PopupRoot"/>'s constructor. /// a specialized dependency resolver into the <see cref="PopupRoot"/>'s constructor.
/// </remarks> /// </remarks>
public IAvaloniaDependencyResolver DependencyResolver public IAvaloniaDependencyResolver? DependencyResolver
{ {
get; get;
set; set;
@ -183,7 +176,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Gets or sets the control that is used to determine the popup's position. /// Gets or sets the control that is used to determine the popup's position.
/// </summary> /// </summary>
public Control PlacementTarget public Control? PlacementTarget
{ {
get { return GetValue(PlacementTargetProperty); } get { return GetValue(PlacementTargetProperty); }
set { SetValue(PlacementTargetProperty, value); } set { SetValue(PlacementTargetProperty, value); }
@ -211,7 +204,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Gets the root of the popup window. /// Gets the root of the popup window.
/// </summary> /// </summary>
IVisual IVisualTreeHost.Root => _popupHost?.HostedVisualTreeRoot; IVisual? IVisualTreeHost.Root => _openState?.PopupHost.HostedVisualTreeRoot;
/// <summary> /// <summary>
/// Opens the popup. /// Opens the popup.
@ -219,50 +212,91 @@ namespace Avalonia.Controls.Primitives
public void Open() public void Open()
{ {
// Popup is currently open // Popup is currently open
if (_topLevel != null) if (_openState != null)
{
return; return;
CloseCurrent(); }
var placementTarget = PlacementTarget ?? this.GetLogicalAncestors().OfType<IVisual>().FirstOrDefault(); var placementTarget = PlacementTarget ?? this.GetLogicalAncestors().OfType<IVisual>().FirstOrDefault();
if (placementTarget == null) if (placementTarget == null)
{
throw new InvalidOperationException("Popup has no logical parent and PlacementTarget is null"); throw new InvalidOperationException("Popup has no logical parent and PlacementTarget is null");
}
_topLevel = placementTarget.GetVisualRoot() as TopLevel; var topLevel = placementTarget.VisualRoot as TopLevel;
if (_topLevel == null) if (topLevel == null)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
"Attempted to open a popup not attached to a TopLevel"); "Attempted to open a popup not attached to a TopLevel");
} }
_popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver); var popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver);
var handlerCleanup = new CompositeDisposable(5);
_bindings.Add(_popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty, void DeferCleanup(IDisposable? disposable)
{
if (disposable is null)
{
return;
}
handlerCleanup.Add(disposable);
}
DeferCleanup(popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty,
HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty)); HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty));
_popupHost.SetChild(Child); popupHost.SetChild(Child);
((ISetLogicalParent)_popupHost).SetParent(this); ((ISetLogicalParent)popupHost).SetParent(this);
_popupHost.ConfigurePosition(placementTarget,
PlacementMode, new Point(HorizontalOffset, VerticalOffset)); popupHost.ConfigurePosition(
_popupHost.TemplateApplied += RootTemplateApplied; placementTarget,
PlacementMode,
var window = _topLevel as Window; new Point(HorizontalOffset, VerticalOffset));
if (window != null)
DeferCleanup(SubscribeToEventHandler<IPopupHost, EventHandler<TemplateAppliedEventArgs>>(popupHost, RootTemplateApplied,
(x, handler) => x.TemplateApplied += handler,
(x, handler) => x.TemplateApplied -= handler));
if (topLevel is Window window)
{ {
window.Deactivated += WindowDeactivated; DeferCleanup(SubscribeToEventHandler<Window, EventHandler>(window, WindowDeactivated,
(x, handler) => x.Deactivated += handler,
(x, handler) => x.Deactivated -= handler));
} }
else else
{ {
var parentPopuproot = _topLevel as PopupRoot; var parentPopupRoot = topLevel as PopupRoot;
if (parentPopuproot?.Parent is Popup popup)
if (parentPopupRoot?.Parent is Popup popup)
{ {
popup.Closed += ParentClosed; DeferCleanup(SubscribeToEventHandler<Popup, EventHandler>(popup, ParentClosed,
(x, handler) => x.Closed += handler,
(x, handler) => x.Closed -= handler));
} }
} }
_topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel);
_nonClientListener = InputManager.Instance?.Process.Subscribe(ListenForNonClientClick);
_popupHost.Show(); DeferCleanup(topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel));
DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick));
var cleanupPopup = Disposable.Create((popupHost, handlerCleanup), state =>
{
state.handlerCleanup.Dispose();
state.popupHost.SetChild(null);
state.popupHost.Hide();
((ISetLogicalParent)state.popupHost).SetParent(null);
state.popupHost.Dispose();
});
_openState = new PopupOpenState(topLevel, popupHost, cleanupPopup);
popupHost.Show();
using (BeginIgnoringIsOpen()) using (BeginIgnoringIsOpen())
{ {
@ -277,14 +311,19 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
public void Close() public void Close()
{ {
if (_popupHost != null) if (_openState is null)
{ {
_popupHost.TemplateApplied -= RootTemplateApplied; using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
return;
} }
_presenterSubscription?.Dispose(); _openState.Dispose();
_openState = null;
CloseCurrent();
using (BeginIgnoringIsOpen()) using (BeginIgnoringIsOpen())
{ {
IsOpen = false; IsOpen = false;
@ -293,41 +332,6 @@ namespace Avalonia.Controls.Primitives
Closed?.Invoke(this, EventArgs.Empty); Closed?.Invoke(this, EventArgs.Empty);
} }
void CloseCurrent()
{
if (_topLevel != null)
{
_topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside);
var window = _topLevel as Window;
if (window != null)
window.Deactivated -= WindowDeactivated;
else
{
var parentPopuproot = _topLevel as PopupRoot;
if (parentPopuproot?.Parent is Popup popup)
{
popup.Closed -= ParentClosed;
}
}
_nonClientListener?.Dispose();
_nonClientListener = null;
_topLevel = null;
}
if (_popupHost != null)
{
foreach(var b in _bindings)
b.Dispose();
_bindings.Clear();
_popupHost.SetChild(null);
_popupHost.Hide();
((ISetLogicalParent)_popupHost).SetParent(null);
_popupHost.Dispose();
_popupHost = null;
}
}
/// <summary> /// <summary>
/// Measures the control. /// Measures the control.
/// </summary> /// </summary>
@ -345,16 +349,22 @@ namespace Avalonia.Controls.Primitives
Close(); Close();
} }
private static IDisposable SubscribeToEventHandler<T, TEventHandler>(T target, TEventHandler handler, Action<T, TEventHandler> subscribe, Action<T, TEventHandler> unsubscribe)
{
subscribe(target, handler);
return Disposable.Create((unsubscribe, target, handler), state => state.unsubscribe(state.target, state.handler));
}
/// <summary> /// <summary>
/// Called when the <see cref="IsOpen"/> property changes. /// Called when the <see cref="IsOpen"/> property changes.
/// </summary> /// </summary>
/// <param name="e">The event args.</param> /// <param name="e">The event args.</param>
private void IsOpenChanged(AvaloniaPropertyChangedEventArgs e) private void IsOpenChanged(AvaloniaPropertyChangedEventArgs<bool> e)
{ {
if (!_ignoreIsOpenChanged) if (!_ignoreIsOpenChanged)
{ {
if ((bool)e.NewValue) if (e.NewValue.Value)
{ {
Open(); Open();
} }
@ -373,7 +383,7 @@ namespace Avalonia.Controls.Primitives
{ {
LogicalChildren.Clear(); LogicalChildren.Clear();
((ISetLogicalParent)e.OldValue)?.SetParent(null); ((ISetLogicalParent?)e.OldValue)?.SetParent(null);
if (e.NewValue != null) if (e.NewValue != null)
{ {
@ -394,34 +404,37 @@ namespace Avalonia.Controls.Primitives
private void PointerPressedOutside(object sender, PointerPressedEventArgs e) private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
{ {
if (!StaysOpen) if (!StaysOpen && !IsChildOrThis((IVisual)e.Source))
{ {
if (!IsChildOrThis((IVisual)e.Source)) Close();
{ e.Handled = true;
Close();
e.Handled = true;
}
} }
} }
private void RootTemplateApplied(object sender, TemplateAppliedEventArgs e) private void RootTemplateApplied(object sender, TemplateAppliedEventArgs e)
{ {
_popupHost.TemplateApplied -= RootTemplateApplied; if (_openState is null)
if (_presenterSubscription != null)
{ {
_presenterSubscription.Dispose(); return;
_presenterSubscription = null;
} }
var popupHost = _openState.PopupHost;
popupHost.TemplateApplied -= RootTemplateApplied;
_openState.SetPresenterSubscription(null);
// If the Popup appears in a control template, then the child controls // If the Popup appears in a control template, then the child controls
// that appear in the popup host need to have their TemplatedParent // that appear in the popup host need to have their TemplatedParent
// properties set. // properties set.
if (TemplatedParent != null) if (TemplatedParent != null && popupHost.Presenter != null)
{ {
_popupHost.Presenter?.ApplyTemplate(); popupHost.Presenter.ApplyTemplate();
_popupHost.Presenter?.GetObservable(ContentPresenter.ChildProperty)
var presenterSubscription = popupHost.Presenter.GetObservable(ContentPresenter.ChildProperty)
.Subscribe(SetTemplatedParentAndApplyChildTemplates); .Subscribe(SetTemplatedParentAndApplyChildTemplates);
_openState.SetPresenterSubscription(presenterSubscription);
} }
} }
@ -440,7 +453,7 @@ namespace Avalonia.Controls.Primitives
if (!(control is IPresenter) && control.TemplatedParent == templatedParent) if (!(control is IPresenter) && control.TemplatedParent == templatedParent)
{ {
foreach (IControl child in control.GetVisualChildren()) foreach (IControl child in control.VisualChildren)
{ {
SetTemplatedParentAndApplyChildTemplates(child); SetTemplatedParentAndApplyChildTemplates(child);
} }
@ -450,22 +463,41 @@ namespace Avalonia.Controls.Primitives
private bool IsChildOrThis(IVisual child) private bool IsChildOrThis(IVisual child)
{ {
IVisual root = child.GetVisualRoot(); if (_openState is null)
while (root is IHostedVisualTreeRoot hostedRoot )
{ {
if (root == this._popupHost) return false;
}
var popupHost = _openState.PopupHost;
IVisual? root = child.VisualRoot;
while (root is IHostedVisualTreeRoot hostedRoot)
{
if (root == popupHost)
{
return true; return true;
root = hostedRoot.Host?.GetVisualRoot(); }
root = hostedRoot.Host?.VisualRoot;
} }
return false; return false;
} }
public bool IsInsidePopup(IVisual visual) public bool IsInsidePopup(IVisual visual)
{ {
return _popupHost != null && ((IVisual)_popupHost)?.IsVisualAncestorOf(visual) == true; if (_openState is null)
{
return false;
}
var popupHost = _openState.PopupHost;
return popupHost != null && ((IVisual)popupHost).IsVisualAncestorOf(visual);
} }
public bool IsPointerOverPopup => ((IInputElement)_popupHost).IsPointerOver; public bool IsPointerOverPopup => ((IInputElement?)_openState?.PopupHost)?.IsPointerOver ?? false;
private void WindowDeactivated(object sender, EventArgs e) private void WindowDeactivated(object sender, EventArgs e)
{ {
@ -503,5 +535,36 @@ namespace Avalonia.Controls.Primitives
_owner._ignoreIsOpenChanged = false; _owner._ignoreIsOpenChanged = false;
} }
} }
private class PopupOpenState : IDisposable
{
private readonly IDisposable _cleanup;
private IDisposable? _presenterCleanup;
public PopupOpenState(TopLevel topLevel, IPopupHost popupHost, IDisposable cleanup)
{
TopLevel = topLevel;
PopupHost = popupHost;
_cleanup = cleanup;
}
public TopLevel TopLevel { get; }
public IPopupHost PopupHost { get; }
public void SetPresenterSubscription(IDisposable? presenterCleanup)
{
_presenterCleanup?.Dispose();
_presenterCleanup = presenterCleanup;
}
public void Dispose()
{
_presenterCleanup?.Dispose();
_cleanup.Dispose();
}
}
} }
} }

24
src/Avalonia.Controls/Primitives/ScrollBar.cs

@ -55,9 +55,6 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
static ScrollBar() static ScrollBar()
{ {
PseudoClass<ScrollBar, Orientation>(OrientationProperty, o => o == Orientation.Vertical, ":vertical");
PseudoClass<ScrollBar, Orientation>(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal");
Thumb.DragDeltaEvent.AddClassHandler<ScrollBar>((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble); Thumb.DragDeltaEvent.AddClassHandler<ScrollBar>((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble);
Thumb.DragCompletedEvent.AddClassHandler<ScrollBar>((x, e) => x.OnThumbDragComplete(e), RoutingStrategies.Bubble); Thumb.DragCompletedEvent.AddClassHandler<ScrollBar>((x, e) => x.OnThumbDragComplete(e), RoutingStrategies.Bubble);
} }
@ -74,6 +71,7 @@ namespace Avalonia.Controls.Primitives
this.GetObservable(VisibilityProperty).Select(_ => Unit.Default)) this.GetObservable(VisibilityProperty).Select(_ => Unit.Default))
.Select(_ => CalculateIsVisible()); .Select(_ => CalculateIsVisible());
this.Bind(IsVisibleProperty, isVisible, BindingPriority.Style); this.Bind(IsVisibleProperty, isVisible, BindingPriority.Style);
UpdatePseudoClasses(Orientation);
} }
/// <summary> /// <summary>
@ -143,6 +141,20 @@ namespace Avalonia.Controls.Primitives
} }
} }
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
if (property == OrientationProperty)
{
UpdatePseudoClasses(newValue.GetValueOrDefault<Orientation>());
}
}
protected override void OnTemplateApplied(TemplateAppliedEventArgs e) protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{ {
base.OnTemplateApplied(e); base.OnTemplateApplied(e);
@ -252,5 +264,11 @@ namespace Avalonia.Controls.Primitives
{ {
Scroll?.Invoke(this, new ScrollEventArgs(scrollEventType, Value)); Scroll?.Invoke(this, new ScrollEventArgs(scrollEventType, Value));
} }
private void UpdatePseudoClasses(Orientation o)
{
PseudoClasses.Set(":vertical", o == Orientation.Vertical);
PseudoClasses.Set(":horizontal", o == Orientation.Horizontal);
}
} }
} }

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

@ -939,8 +939,8 @@ namespace Avalonia.Controls.Primitives
{ {
var changed = new SelectionChangedEventArgs( var changed = new SelectionChangedEventArgs(
SelectionChangedEvent, SelectionChangedEvent,
added ?? Empty, removed ?? Empty,
removed ?? Empty); added ?? Empty);
RaiseEvent(changed); RaiseEvent(changed);
} }
} }
@ -1055,8 +1055,8 @@ namespace Avalonia.Controls.Primitives
var e = new SelectionChangedEventArgs( var e = new SelectionChangedEventArgs(
SelectionChangedEvent, SelectionChangedEvent,
added != -1 ? new[] { ElementAt(Items, added) } : Array.Empty<object>(), removed?.Select(x => ElementAt(Items, x)).ToArray() ?? Array.Empty<object>(),
removed?.Select(x => ElementAt(Items, x)).ToArray() ?? Array.Empty<object>()); added != -1 ? new[] { ElementAt(Items, added) } : Array.Empty<object>());
RaiseEvent(e); RaiseEvent(e);
} }

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

@ -2,8 +2,8 @@
// 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 Avalonia.Interactivity;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Interactivity;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
@ -51,13 +51,14 @@ namespace Avalonia.Controls.Primitives
static ToggleButton() static ToggleButton()
{ {
PseudoClass<ToggleButton, bool?>(IsCheckedProperty, c => c == true, ":checked");
PseudoClass<ToggleButton, bool?>(IsCheckedProperty, c => c == false, ":unchecked");
PseudoClass<ToggleButton, bool?>(IsCheckedProperty, c => c == null, ":indeterminate");
IsCheckedProperty.Changed.AddClassHandler<ToggleButton>((x, e) => x.OnIsCheckedChanged(e)); IsCheckedProperty.Changed.AddClassHandler<ToggleButton>((x, e) => x.OnIsCheckedChanged(e));
} }
public ToggleButton()
{
UpdatePseudoClasses(IsChecked);
}
/// <summary> /// <summary>
/// Raised when a <see cref="ToggleButton"/> is checked. /// Raised when a <see cref="ToggleButton"/> is checked.
/// </summary> /// </summary>
@ -91,7 +92,11 @@ namespace Avalonia.Controls.Primitives
public bool? IsChecked public bool? IsChecked
{ {
get => _isChecked; get => _isChecked;
set => SetAndRaise(IsCheckedProperty, ref _isChecked, value); set
{
SetAndRaise(IsCheckedProperty, ref _isChecked, value);
UpdatePseudoClasses(value);
}
} }
/// <summary> /// <summary>
@ -182,5 +187,12 @@ namespace Avalonia.Controls.Primitives
break; break;
} }
} }
private void UpdatePseudoClasses(bool? isChecked)
{
PseudoClasses.Set(":checked", isChecked == true);
PseudoClasses.Set(":unchecked", isChecked == false);
PseudoClasses.Set(":indeterminate", isChecked == null);
}
} }
} }

28
src/Avalonia.Controls/Primitives/Track.cs

@ -4,6 +4,7 @@
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System; using System;
using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Metadata; using Avalonia.Metadata;
@ -46,14 +47,17 @@ namespace Avalonia.Controls.Primitives
static Track() static Track()
{ {
PseudoClass<Track, Orientation>(OrientationProperty, o => o == Orientation.Vertical, ":vertical");
PseudoClass<Track, Orientation>(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal");
ThumbProperty.Changed.AddClassHandler<Track>((x,e) => x.ThumbChanged(e)); ThumbProperty.Changed.AddClassHandler<Track>((x,e) => x.ThumbChanged(e));
IncreaseButtonProperty.Changed.AddClassHandler<Track>((x, e) => x.ButtonChanged(e)); IncreaseButtonProperty.Changed.AddClassHandler<Track>((x, e) => x.ButtonChanged(e));
DecreaseButtonProperty.Changed.AddClassHandler<Track>((x, e) => x.ButtonChanged(e)); DecreaseButtonProperty.Changed.AddClassHandler<Track>((x, e) => x.ButtonChanged(e));
AffectsArrange<Track>(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty); AffectsArrange<Track>(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty);
} }
public Track()
{
UpdatePseudoClasses(Orientation);
}
public double Minimum public double Minimum
{ {
get { return _minimum; } get { return _minimum; }
@ -276,6 +280,20 @@ namespace Avalonia.Controls.Primitives
return arrangeSize; return arrangeSize;
} }
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
if (property == OrientationProperty)
{
UpdatePseudoClasses(newValue.GetValueOrDefault<Orientation>());
}
}
private static void CoerceLength(ref double componentLength, double trackLength) private static void CoerceLength(ref double componentLength, double trackLength)
{ {
if (componentLength < 0) if (componentLength < 0)
@ -433,5 +451,11 @@ namespace Avalonia.Controls.Primitives
DecreaseButton.IsVisible = visible; DecreaseButton.IsVisible = visible;
} }
} }
private void UpdatePseudoClasses(Orientation o)
{
PseudoClasses.Set(":vertical", o == Orientation.Vertical);
PseudoClasses.Set(":horizontal", o == Orientation.Horizontal);
}
} }
} }

53
src/Avalonia.Controls/ProgressBar.cs

@ -4,6 +4,7 @@
using System; using System;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Layout; using Avalonia.Layout;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -16,6 +17,9 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> IsIndeterminateProperty = public static readonly StyledProperty<bool> IsIndeterminateProperty =
AvaloniaProperty.Register<ProgressBar, bool>(nameof(IsIndeterminate)); AvaloniaProperty.Register<ProgressBar, bool>(nameof(IsIndeterminate));
public static readonly StyledProperty<bool> ShowProgressTextProperty =
AvaloniaProperty.Register<ProgressBar, bool>(nameof(ShowProgressText));
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);
@ -35,20 +39,27 @@ namespace Avalonia.Controls
static ProgressBar() static ProgressBar()
{ {
PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Orientation.Vertical, ":vertical");
PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal");
PseudoClass<ProgressBar>(IsIndeterminateProperty, ":indeterminate");
ValueProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e)); ValueProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));
IsIndeterminateProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e)); IsIndeterminateProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));
} }
public ProgressBar()
{
UpdatePseudoClasses(IsIndeterminate, Orientation);
}
public bool IsIndeterminate public bool IsIndeterminate
{ {
get => GetValue(IsIndeterminateProperty); get => GetValue(IsIndeterminateProperty);
set => SetValue(IsIndeterminateProperty, value); set => SetValue(IsIndeterminateProperty, value);
} }
public bool ShowProgressText
{
get => GetValue(ShowProgressTextProperty);
set => SetValue(ShowProgressTextProperty, value);
}
public Orientation Orientation public Orientation Orientation
{ {
get => GetValue(OrientationProperty); get => GetValue(OrientationProperty);
@ -75,6 +86,24 @@ namespace Avalonia.Controls
return base.ArrangeOverride(finalSize); return base.ArrangeOverride(finalSize);
} }
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
if (property == IsIndeterminateProperty)
{
UpdatePseudoClasses(newValue.GetValueOrDefault<bool>(), null);
}
else if (property == OrientationProperty)
{
UpdatePseudoClasses(null, newValue.GetValueOrDefault<Orientation>());
}
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e) protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{ {
@ -121,5 +150,21 @@ namespace Avalonia.Controls
{ {
UpdateIndicator(Bounds.Size); UpdateIndicator(Bounds.Size);
} }
private void UpdatePseudoClasses(
bool? isIndeterminate,
Orientation? o)
{
if (isIndeterminate.HasValue)
{
PseudoClasses.Set(":indeterminate", isIndeterminate.Value);
}
if (o.HasValue)
{
PseudoClasses.Set(":vertical", o == Orientation.Vertical);
PseudoClasses.Set(":horizontal", o == Orientation.Horizontal);
}
}
} }
} }

4
src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs

@ -12,11 +12,11 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs
{ {
internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int newIndex, int oldIndex) internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int oldIndex, int newIndex)
{ {
Element = element; Element = element;
NewIndex = newIndex;
OldIndex = oldIndex; OldIndex = oldIndex;
NewIndex = newIndex;
} }
/// <summary> /// <summary>

16
src/Avalonia.Controls/ScrollViewer.cs

@ -249,6 +249,22 @@ namespace Avalonia.Controls
set { SetValue(VerticalScrollBarVisibilityProperty, value); } set { SetValue(VerticalScrollBarVisibilityProperty, value); }
} }
/// <summary>
/// Scrolls to the top-left corner of the content.
/// </summary>
public void ScrollToHome()
{
Offset = new Vector(double.NegativeInfinity, double.NegativeInfinity);
}
/// <summary>
/// Scrolls to the bottom-left corner of the content.
/// </summary>
public void ScrollToEnd()
{
Offset = new Vector(double.NegativeInfinity, double.PositiveInfinity);
}
/// <summary> /// <summary>
/// Gets a value indicating whether the viewer can scroll horizontally. /// Gets a value indicating whether the viewer can scroll horizontally.
/// </summary> /// </summary>

6
src/Avalonia.Controls/SelectionChangedEventArgs.cs

@ -16,13 +16,13 @@ namespace Avalonia.Controls
/// Initializes a new instance of the <see cref="SelectionChangedEventArgs"/> class. /// Initializes a new instance of the <see cref="SelectionChangedEventArgs"/> class.
/// </summary> /// </summary>
/// <param name="routedEvent">The event being raised.</param> /// <param name="routedEvent">The event being raised.</param>
/// <param name="addedItems">The items added to the selection.</param>
/// <param name="removedItems">The items removed from the selection.</param> /// <param name="removedItems">The items removed from the selection.</param>
public SelectionChangedEventArgs(RoutedEvent routedEvent, IList addedItems, IList removedItems) /// <param name="addedItems">The items added to the selection.</param>
public SelectionChangedEventArgs(RoutedEvent routedEvent, IList removedItems, IList addedItems)
: base(routedEvent) : base(routedEvent)
{ {
AddedItems = addedItems;
RemovedItems = removedItems; RemovedItems = removedItems;
AddedItems = addedItems;
} }
/// <summary> /// <summary>

24
src/Avalonia.Controls/Slider.cs

@ -3,6 +3,7 @@
using System; using System;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
@ -43,8 +44,6 @@ namespace Avalonia.Controls
static Slider() static Slider()
{ {
OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal); OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal);
PseudoClass<Slider, Orientation>(OrientationProperty, o => o == Orientation.Vertical, ":vertical");
PseudoClass<Slider, Orientation>(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal");
Thumb.DragStartedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragStarted(e), RoutingStrategies.Bubble); Thumb.DragStartedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragStarted(e), RoutingStrategies.Bubble);
Thumb.DragDeltaEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble); Thumb.DragDeltaEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble);
Thumb.DragCompletedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragCompleted(e), RoutingStrategies.Bubble); Thumb.DragCompletedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragCompleted(e), RoutingStrategies.Bubble);
@ -55,6 +54,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public Slider() public Slider()
{ {
UpdatePseudoClasses(Orientation);
} }
/// <summary> /// <summary>
@ -137,6 +137,20 @@ namespace Avalonia.Controls
} }
} }
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
if (property == OrientationProperty)
{
UpdatePseudoClasses(newValue.GetValueOrDefault<Orientation>());
}
}
/// <summary> /// <summary>
/// Called when user start dragging the <see cref="Thumb"/>. /// Called when user start dragging the <see cref="Thumb"/>.
/// </summary> /// </summary>
@ -190,5 +204,11 @@ namespace Avalonia.Controls
return value; return value;
} }
private void UpdatePseudoClasses(Orientation o)
{
PseudoClasses.Set(":vertical", o == Orientation.Vertical);
PseudoClasses.Set(":horizontal", o == Orientation.Horizontal);
}
} }
} }

42
src/Avalonia.Controls/TextBox.cs

@ -14,6 +14,7 @@ using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -72,6 +73,18 @@ namespace Avalonia.Controls
public static readonly StyledProperty<TextAlignment> TextAlignmentProperty = public static readonly StyledProperty<TextAlignment> TextAlignmentProperty =
TextBlock.TextAlignmentProperty.AddOwner<TextBox>(); TextBlock.TextAlignmentProperty.AddOwner<TextBox>();
/// <summary>
/// Defines the <see cref="HorizontalAlignment"/> property.
/// </summary>
public static readonly StyledProperty<HorizontalAlignment> HorizontalContentAlignmentProperty =
ContentControl.HorizontalContentAlignmentProperty.AddOwner<TextBox>();
/// <summary>
/// Defines the <see cref="VerticalAlignment"/> property.
/// </summary>
public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
ContentControl.VerticalContentAlignmentProperty.AddOwner<TextBox>();
public static readonly StyledProperty<TextWrapping> TextWrappingProperty = public static readonly StyledProperty<TextWrapping> TextWrappingProperty =
TextBlock.TextWrappingProperty.AddOwner<TextBox>(); TextBlock.TextWrappingProperty.AddOwner<TextBox>();
@ -262,6 +275,24 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Gets or sets the horizontal alignment of the content within the control.
/// </summary>
public HorizontalAlignment HorizontalContentAlignment
{
get { return GetValue(HorizontalContentAlignmentProperty); }
set { SetValue(HorizontalContentAlignmentProperty, value); }
}
/// <summary>
/// Gets or sets the vertical alignment of the content within the control.
/// </summary>
public VerticalAlignment VerticalContentAlignment
{
get { return GetValue(VerticalContentAlignmentProperty); }
set { SetValue(VerticalContentAlignmentProperty, value); }
}
public TextAlignment TextAlignment public TextAlignment TextAlignment
{ {
get { return GetValue(TextAlignmentProperty); } get { return GetValue(TextAlignmentProperty); }
@ -316,8 +347,7 @@ namespace Avalonia.Controls
!AcceptsReturn && !AcceptsReturn &&
Text?.Length > 0) Text?.Length > 0)
{ {
SelectionStart = 0; SelectAll();
SelectionEnd = Text.Length;
} }
else else
{ {
@ -673,8 +703,7 @@ namespace Avalonia.Controls
SelectionEnd = StringUtils.NextWord(text, index); SelectionEnd = StringUtils.NextWord(text, index);
break; break;
case 3: case 3:
SelectionStart = 0; SelectAll();
SelectionEnd = text.Length;
break; break;
} }
} }
@ -896,7 +925,10 @@ namespace Avalonia.Controls
CaretIndex = caretIndex; CaretIndex = caretIndex;
} }
private void SelectAll() /// <summary>
/// Select all text in the TextBox
/// </summary>
public void SelectAll()
{ {
SelectionStart = 0; SelectionStart = 0;
SelectionEnd = Text?.Length ?? 0; SelectionEnd = Text?.Length ?? 0;

4
src/Avalonia.Controls/TreeView.cs

@ -324,8 +324,8 @@ namespace Avalonia.Controls
{ {
var changed = new SelectionChangedEventArgs( var changed = new SelectionChangedEventArgs(
SelectingItemsControl.SelectionChangedEvent, SelectingItemsControl.SelectionChangedEvent,
added ?? Empty, removed ?? Empty,
removed ?? Empty); added ?? Empty);
RaiseEvent(changed); RaiseEvent(changed);
} }
} }

4
src/Avalonia.Controls/Window.cs

@ -129,7 +129,7 @@ namespace Avalonia.Controls
ShowInTaskbarProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue)); ShowInTaskbarProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue));
IconProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetIcon(((WindowIcon)e.NewValue).PlatformImpl)); IconProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetIcon(((WindowIcon)e.NewValue)?.PlatformImpl));
CanResizeProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.CanResize((bool)e.NewValue)); CanResizeProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.CanResize((bool)e.NewValue));
@ -529,7 +529,7 @@ namespace Avalonia.Controls
{ {
var sizeToContent = SizeToContent; var sizeToContent = SizeToContent;
var clientSize = ClientSize; var clientSize = ClientSize;
Size constraint = clientSize; var constraint = availableSize;
if ((sizeToContent & SizeToContent.Width) != 0) if ((sizeToContent & SizeToContent.Width) != 0)
{ {

2
src/Avalonia.Input/IInputElement.cs

@ -63,7 +63,7 @@ namespace Avalonia.Input
event EventHandler<PointerReleasedEventArgs> PointerReleased; event EventHandler<PointerReleasedEventArgs> PointerReleased;
/// <summary> /// <summary>
/// Occurs when the mouse wheen is scrolled over the control. /// Occurs when the mouse wheel is scrolled over the control.
/// </summary> /// </summary>
event EventHandler<PointerWheelEventArgs> PointerWheelChanged; event EventHandler<PointerWheelEventArgs> PointerWheelChanged;

42
src/Avalonia.Input/InputElement.cs

@ -4,6 +4,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Input.GestureRecognizers; using Avalonia.Input.GestureRecognizers;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -181,10 +183,11 @@ namespace Avalonia.Input
PointerReleasedEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerReleased(e)); PointerReleasedEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerReleased(e));
PointerCaptureLostEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerCaptureLost(e)); PointerCaptureLostEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerCaptureLost(e));
PointerWheelChangedEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerWheelChanged(e)); PointerWheelChangedEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerWheelChanged(e));
}
PseudoClass<InputElement, bool>(IsEffectivelyEnabledProperty, x => !x, ":disabled"); public InputElement()
PseudoClass<InputElement>(IsFocusedProperty, ":focus"); {
PseudoClass<InputElement>(IsPointerOverProperty, ":pointerover"); UpdatePseudoClasses(IsFocused, IsPointerOver);
} }
/// <summary> /// <summary>
@ -372,7 +375,11 @@ namespace Avalonia.Input
public bool IsEffectivelyEnabled public bool IsEffectivelyEnabled
{ {
get => _isEffectivelyEnabled; get => _isEffectivelyEnabled;
private set => SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value); private set
{
SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value);
PseudoClasses.Set(":disabled", !value);
}
} }
public List<KeyBinding> KeyBindings { get; } = new List<KeyBinding>(); public List<KeyBinding> KeyBindings { get; } = new List<KeyBinding>();
@ -522,6 +529,20 @@ namespace Avalonia.Input
{ {
} }
protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
if (property == IsFocusedProperty)
{
UpdatePseudoClasses(newValue.GetValueOrDefault<bool>(), null);
}
else if (property == IsPointerOverProperty)
{
UpdatePseudoClasses(null, newValue.GetValueOrDefault<bool>());
}
}
/// <summary> /// <summary>
/// Updates the <see cref="IsEffectivelyEnabled"/> property value according to the parent /// Updates the <see cref="IsEffectivelyEnabled"/> property value according to the parent
/// control's enabled state and <see cref="IsEnabledCore"/>. /// control's enabled state and <see cref="IsEnabledCore"/>.
@ -578,5 +599,18 @@ namespace Avalonia.Input
child?.UpdateIsEffectivelyEnabled(this); child?.UpdateIsEffectivelyEnabled(this);
} }
} }
private void UpdatePseudoClasses(bool? isFocused, bool? isPointerOver)
{
if (isFocused.HasValue)
{
PseudoClasses.Set(":focus", isFocused.Value);
}
if (isPointerOver.HasValue)
{
PseudoClasses.Set(":pointerover", isPointerOver.Value);
}
}
} }
} }

12
src/Avalonia.Layout/LayoutManager.cs

@ -132,8 +132,16 @@ namespace Avalonia.Layout
/// <inheritdoc/> /// <inheritdoc/>
public void ExecuteInitialLayoutPass(ILayoutRoot root) public void ExecuteInitialLayoutPass(ILayoutRoot root)
{ {
Measure(root); try
Arrange(root); {
_running = true;
Measure(root);
Arrange(root);
}
finally
{
_running = false;
}
// Running the initial layout pass may have caused some control to be invalidated // Running the initial layout pass may have caused some control to be invalidated
// so run a full layout pass now (this usually due to scrollbars; its not known // so run a full layout pass now (this usually due to scrollbars; its not known

11
src/Avalonia.Native/WindowImplBase.cs

@ -162,9 +162,12 @@ namespace Avalonia.Native
void IAvnWindowBaseEvents.Resized(AvnSize size) void IAvnWindowBaseEvents.Resized(AvnSize size)
{ {
var s = new Size(size.Width, size.Height); if (_parent._native != null)
_parent._savedLogicalSize = s; {
_parent.Resized?.Invoke(s); var s = new Size(size.Width, size.Height);
_parent._savedLogicalSize = s;
_parent.Resized?.Invoke(s);
}
} }
void IAvnWindowBaseEvents.PositionChanged(AvnPoint position) void IAvnWindowBaseEvents.PositionChanged(AvnPoint position)
@ -317,7 +320,7 @@ namespace Avalonia.Native
_native.SetTopMost(value); _native.SetTopMost(value);
} }
public double Scaling => _native.GetScaling(); public double Scaling => _native?.GetScaling() ?? 1;
public Action Deactivated { get; set; } public Action Deactivated { get; set; }
public Action Activated { get; set; } public Action Activated { get; set; }

28
src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs

@ -0,0 +1,28 @@
using System;
namespace Avalonia.Controls
{
public static class PseudolassesExtensions
{
/// <summary>
/// Adds or removes a pseudoclass depending on a boolean value.
/// </summary>
/// <param name="classes">The pseudoclasses collection.</param>
/// <param name="name">The name of the pseudoclass to set.</param>
/// <param name="value">True to add the pseudoclass or false to remove.</param>
public static void Set(this IPseudoClasses classes, string name, bool value)
{
Contract.Requires<ArgumentNullException>(classes != null);
if (value)
{
classes.Add(name);
}
else
{
classes.Remove(name);
}
}
}
}

104
src/Avalonia.Styling/StyledElement.cs

@ -488,83 +488,6 @@ namespace Avalonia
InheritanceParent = parent; InheritanceParent = parent;
} }
/// <summary>
/// Adds a pseudo-class to be set when a property is true.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="className">The pseudo-class.</param>
[Obsolete("Use PseudoClass<TOwner> and specify the control type.")]
protected static void PseudoClass(AvaloniaProperty<bool> property, string className)
{
PseudoClass<StyledElement>(property, className);
}
/// <summary>
/// Adds a pseudo-class to be set when a property is true.
/// </summary>
/// <typeparam name="TOwner">The type to apply the pseudo-class to.</typeparam>
/// <param name="property">The property.</param>
/// <param name="className">The pseudo-class.</param>
protected static void PseudoClass<TOwner>(AvaloniaProperty<bool> property, string className)
where TOwner : class, IStyledElement
{
PseudoClass<TOwner, bool>(property, x => x, className);
}
/// <summary>
/// Adds a pseudo-class to be set when a property equals a certain value.
/// </summary>
/// <typeparam name="TProperty">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="selector">Returns a boolean value based on the property value.</param>
/// <param name="className">The pseudo-class.</param>
[Obsolete("Use PseudoClass<TOwner, TProperty> and specify the control type.")]
protected static void PseudoClass<TProperty>(
AvaloniaProperty<TProperty> property,
Func<TProperty, bool> selector,
string className)
{
PseudoClass<StyledElement, TProperty>(property, selector, className);
}
/// <summary>
/// Adds a pseudo-class to be set when a property equals a certain value.
/// </summary>
/// <typeparam name="TProperty">The type of the property.</typeparam>
/// <typeparam name="TOwner">The type to apply the pseudo-class to.</typeparam>
/// <param name="property">The property.</param>
/// <param name="selector">Returns a boolean value based on the property value.</param>
/// <param name="className">The pseudo-class.</param>
protected static void PseudoClass<TOwner, TProperty>(
AvaloniaProperty<TProperty> property,
Func<TProperty, bool> selector,
string className)
where TOwner : class, IStyledElement
{
Contract.Requires<ArgumentNullException>(property != null);
Contract.Requires<ArgumentNullException>(selector != null);
Contract.Requires<ArgumentNullException>(className != null);
if (string.IsNullOrWhiteSpace(className))
{
throw new ArgumentException("Cannot supply an empty className.");
}
property.Changed.Merge(property.Initialized)
.Where(e => e.Sender is TOwner)
.Subscribe(e =>
{
if (selector((TProperty)e.NewValue))
{
((StyledElement)e.Sender).PseudoClasses.Add(className);
}
else
{
((StyledElement)e.Sender).PseudoClasses.Remove(className);
}
});
}
protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{ {
switch (e.Action) switch (e.Action)
@ -650,9 +573,12 @@ namespace Avalonia
element._dataContextUpdating = true; element._dataContextUpdating = true;
element.OnDataContextBeginUpdate(); element.OnDataContextBeginUpdate();
foreach (var child in element.LogicalChildren) var logicalChildren = element.LogicalChildren;
var logicalChildrenCount = logicalChildren.Count;
for (var i = 0; i < logicalChildrenCount; i++)
{ {
if (child is StyledElement s && if (element.LogicalChildren[i] is StyledElement s &&
s.InheritanceParent == element && s.InheritanceParent == element &&
!s.IsSet(DataContextProperty)) !s.IsSet(DataContextProperty))
{ {
@ -723,9 +649,15 @@ namespace Avalonia
AttachedToLogicalTree?.Invoke(this, e); AttachedToLogicalTree?.Invoke(this, e);
} }
foreach (var child in LogicalChildren.OfType<StyledElement>()) var logicalChildren = LogicalChildren;
var logicalChildrenCount = logicalChildren.Count;
for (var i = 0; i < logicalChildrenCount; i++)
{ {
child.OnAttachedToLogicalTreeCore(e); if (logicalChildren[i] is StyledElement child)
{
child.OnAttachedToLogicalTreeCore(e);
}
} }
} }
@ -738,9 +670,15 @@ namespace Avalonia
OnDetachedFromLogicalTree(e); OnDetachedFromLogicalTree(e);
DetachedFromLogicalTree?.Invoke(this, e); DetachedFromLogicalTree?.Invoke(this, e);
foreach (var child in LogicalChildren.OfType<StyledElement>()) var logicalChildren = LogicalChildren;
var logicalChildrenCount = logicalChildren.Count;
for (var i = 0; i < logicalChildrenCount; i++)
{ {
child.OnDetachedFromLogicalTreeCore(e); if (logicalChildren[i] is StyledElement child)
{
child.OnDetachedFromLogicalTreeCore(e);
}
} }
#if DEBUG #if DEBUG

4
src/Avalonia.Styling/Styling/Setter.cs

@ -78,8 +78,6 @@ namespace Avalonia.Styling
{ {
Contract.Requires<ArgumentNullException>(control != null); Contract.Requires<ArgumentNullException>(control != null);
var description = style?.ToString();
if (Property == null) if (Property == null)
{ {
throw new InvalidOperationException("Setter.Property must be set."); throw new InvalidOperationException("Setter.Property must be set.");
@ -107,6 +105,8 @@ namespace Avalonia.Styling
} }
else else
{ {
var description = style?.ToString();
var activated = new ActivatedValue(activator, value, description); var activated = new ActivatedValue(activator, value, description);
return control.Bind(Property, activated, BindingPriority.StyleTrigger); return control.Bind(Property, activated, BindingPriority.StyleTrigger);
} }

35
src/Avalonia.Styling/Styling/Style.cs

@ -116,13 +116,21 @@ namespace Avalonia.Styling
if (match.IsMatch) if (match.IsMatch)
{ {
var controlSubscriptions = GetSubscriptions(control); var controlSubscriptions = GetSubscriptions(control);
var subs = new CompositeDisposable(Setters.Count + Animations.Count);
if (control is Animatable animatable) var animatable = control as Animatable;
var setters = Setters;
var settersCount = setters.Count;
var animations = Animations;
var animationsCount = animations.Count;
var subs = new CompositeDisposable(settersCount + (animatable != null ? animationsCount : 0) + 1);
if (animatable != null)
{ {
foreach (var animation in Animations) for (var i = 0; i < animationsCount; i++)
{ {
var animation = animations[i];
var obsMatch = match.Activator; var obsMatch = match.Activator;
if (match.Result == SelectorMatchResult.AlwaysThisType || if (match.Result == SelectorMatchResult.AlwaysThisType ||
@ -133,17 +141,19 @@ namespace Avalonia.Styling
var sub = animation.Apply(animatable, null, obsMatch); var sub = animation.Apply(animatable, null, obsMatch);
subs.Add(sub); subs.Add(sub);
} }
} }
foreach (var setter in Setters) for (var i = 0; i < settersCount; i++)
{ {
var setter = setters[i];
var sub = setter.Apply(this, control, match.Activator); var sub = setter.Apply(this, control, match.Activator);
subs.Add(sub); subs.Add(sub);
} }
subs.Add(Disposable.Create((subs, Subscriptions) , state => state.Subscriptions.Remove(state.subs)));
controlSubscriptions.Add(subs); controlSubscriptions.Add(subs);
controlSubscriptions.Add(Disposable.Create(() => Subscriptions.Remove(subs)));
Subscriptions.Add(subs); Subscriptions.Add(subs);
} }
@ -151,18 +161,23 @@ namespace Avalonia.Styling
} }
else if (control == container) else if (control == container)
{ {
var setters = Setters;
var settersCount = setters.Count;
var controlSubscriptions = GetSubscriptions(control); var controlSubscriptions = GetSubscriptions(control);
var subs = new CompositeDisposable(Setters.Count); var subs = new CompositeDisposable(settersCount + 1);
foreach (var setter in Setters) for (var i = 0; i < settersCount; i++)
{ {
var setter = setters[i];
var sub = setter.Apply(this, control, null); var sub = setter.Apply(this, control, null);
subs.Add(sub); subs.Add(sub);
} }
subs.Add(Disposable.Create((subs, Subscriptions), state => state.Subscriptions.Remove(state.subs)));
controlSubscriptions.Add(subs); controlSubscriptions.Add(subs);
controlSubscriptions.Add(Disposable.Create(() => Subscriptions.Remove(subs)));
Subscriptions.Add(subs); Subscriptions.Add(subs);
return true; return true;
} }

4
src/Avalonia.Styling/Styling/Styles.cs

@ -239,8 +239,10 @@ namespace Avalonia.Styling
/// <inheritdoc/> /// <inheritdoc/>
public bool Remove(IStyle item) => _styles.Remove(item); public bool Remove(IStyle item) => _styles.Remove(item);
public AvaloniaList<IStyle>.Enumerator GetEnumerator() => _styles.GetEnumerator();
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerator<IStyle> GetEnumerator() => _styles.GetEnumerator(); IEnumerator<IStyle> IEnumerable<IStyle>.GetEnumerator() => _styles.GetEnumerator();
/// <inheritdoc/> /// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator();

21
src/Avalonia.Themes.Default/ComboBox.xaml

@ -4,6 +4,7 @@
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/> <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
<Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/> <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
<Setter Property="Padding" Value="4"/> <Setter Property="Padding" Value="4"/>
<Setter Property="MinHeight" Value="20"/>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Border Name="border" <Border Name="border"
@ -40,14 +41,14 @@
StaysOpen="False"> StaysOpen="False">
<Border BorderBrush="{DynamicResource ThemeBorderMidBrush}" <Border BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1"> BorderThickness="1">
<ScrollViewer> <ScrollViewer>
<ItemsPresenter Name="PART_ItemsPresenter" <ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}" Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}" ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}" ItemTemplate="{TemplateBinding ItemTemplate}"
VirtualizationMode="{TemplateBinding VirtualizationMode}" VirtualizationMode="{TemplateBinding VirtualizationMode}"
/> />
</ScrollViewer> </ScrollViewer>
</Border> </Border>
</Popup> </Popup>
</Grid> </Grid>
@ -58,7 +59,7 @@
<Style Selector="ComboBox:pointerover /template/ Border#border"> <Style Selector="ComboBox:pointerover /template/ Border#border">
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderHighBrush}"/> <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderHighBrush}"/>
</Style> </Style>
<Style Selector="ComboBox:disabled /template/ Border#border"> <Style Selector="ComboBox:disabled /template/ Border#border">
<Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}" /> <Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}" />
</Style> </Style>
</Styles> </Styles>

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

@ -1,14 +1,25 @@
<Styles xmlns="https://github.com/avaloniaui"> <Styles xmlns="https://github.com/avaloniaui">
<Style Selector="ProgressBar"> <Style Selector="ProgressBar">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/> <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
<Setter Property="Foreground" Value="{DynamicResource ThemeAccentBrush}"/> <Setter Property="Foreground" Value="{DynamicResource ThemeAccentBrush}"/>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Border Background="{TemplateBinding Background}" <Grid>
BorderBrush="{TemplateBinding BorderBrush}" <Border Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"> BorderBrush="{TemplateBinding BorderBrush}"
<Border Name="PART_Indicator" Background="{TemplateBinding Foreground}"/> BorderThickness="{TemplateBinding BorderThickness}">
</Border> <Border Name="PART_Indicator" Background="{TemplateBinding Foreground}"/>
</Border>
<LayoutTransformControl
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsVisible="{Binding ShowProgressText, RelativeSource={RelativeSource TemplatedParent}}"
Name="PART_LayoutTransformControl">
<TextBlock
Foreground="{DynamicResource ThemeForegroundBrush}"
Text="{Binding Value, RelativeSource={RelativeSource TemplatedParent}, StringFormat={}{0:0}%}" />
</LayoutTransformControl>
</Grid>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </Style>
@ -22,42 +33,49 @@
</Style> </Style>
<Style Selector="ProgressBar:horizontal"> <Style Selector="ProgressBar:horizontal">
<Setter Property="MinWidth" Value="200"/> <Setter Property="MinWidth" Value="200"/>
<Setter Property="MinHeight" Value="14"/> <Setter Property="MinHeight" Value="16"/>
</Style> </Style>
<Style Selector="ProgressBar:vertical"> <Style Selector="ProgressBar:vertical">
<Setter Property="MinWidth" Value="14"/> <Setter Property="MinWidth" Value="16"/>
<Setter Property="MinHeight" Value="200"/> <Setter Property="MinHeight" Value="200"/>
</Style> </Style>
<Style Selector="ProgressBar:vertical /template/ LayoutTransformControl#PART_LayoutTransformControl">
<Setter Property="LayoutTransform">
<Setter.Value>
<RotateTransform Angle="90"/>
</Setter.Value>
</Setter>
</Style>
<Style Selector="ProgressBar:horizontal:indeterminate /template/ Border#PART_Indicator"> <Style Selector="ProgressBar:horizontal:indeterminate /template/ Border#PART_Indicator">
<Style.Animations> <Style.Animations>
<Animation Duration="0:0:3" <Animation Duration="0:0:3"
IterationCount="Infinite" IterationCount="Infinite"
Easing="LinearEasing"> Easing="LinearEasing">
<KeyFrame Cue="0%"> <KeyFrame Cue="0%">
<Setter Property="TranslateTransform.X" <Setter Property="TranslateTransform.X"
Value="{Binding IndeterminateStartingOffset, RelativeSource={RelativeSource TemplatedParent}}" /> Value="{Binding IndeterminateStartingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
</KeyFrame> </KeyFrame>
<KeyFrame Cue="100%"> <KeyFrame Cue="100%">
<Setter Property="TranslateTransform.X" <Setter Property="TranslateTransform.X"
Value="{Binding IndeterminateEndingOffset, RelativeSource={RelativeSource TemplatedParent}}" /> Value="{Binding IndeterminateEndingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
</KeyFrame> </KeyFrame>
</Animation> </Animation>
</Style.Animations> </Style.Animations>
</Style> </Style>
<Style Selector="ProgressBar:vertical:indeterminate /template/ Border#PART_Indicator"> <Style Selector="ProgressBar:vertical:indeterminate /template/ Border#PART_Indicator">
<Style.Animations> <Style.Animations>
<Animation Duration="0:0:3" <Animation Duration="0:0:3"
IterationCount="Infinite" IterationCount="Infinite"
Easing="LinearEasing"> Easing="LinearEasing">
<KeyFrame Cue="0%"> <KeyFrame Cue="0%">
<Setter Property="TranslateTransform.Y" <Setter Property="TranslateTransform.Y"
Value="{Binding IndeterminateStartingOffset, RelativeSource={RelativeSource TemplatedParent}}" /> Value="{Binding IndeterminateStartingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
</KeyFrame> </KeyFrame>
<KeyFrame Cue="100%"> <KeyFrame Cue="100%">
<Setter Property="TranslateTransform.Y" <Setter Property="TranslateTransform.Y"
Value="{Binding IndeterminateEndingOffset, RelativeSource={RelativeSource TemplatedParent}}" /> Value="{Binding IndeterminateEndingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
</KeyFrame> </KeyFrame>
</Animation> </Animation>
</Style.Animations> </Style.Animations>
</Style> </Style>
</Styles> </Styles>

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

@ -12,7 +12,9 @@
Background="{TemplateBinding Background}" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"> BorderThickness="{TemplateBinding BorderThickness}">
<DockPanel Margin="{TemplateBinding Padding}"> <DockPanel Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
<TextBlock Name="floatingWatermark" <TextBlock Name="floatingWatermark"
Foreground="{DynamicResource ThemeAccentBrush}" Foreground="{DynamicResource ThemeAccentBrush}"

5
src/Avalonia.Visuals/Media/ArcSegment.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 System.Globalization;
namespace Avalonia.Media namespace Avalonia.Media
{ {
public sealed class ArcSegment : PathSegment public sealed class ArcSegment : PathSegment
@ -99,5 +101,8 @@ namespace Avalonia.Media
{ {
ctx.ArcTo(Point, Size, RotationAngle, IsLargeArc, SweepDirection); ctx.ArcTo(Point, Size, RotationAngle, IsLargeArc, SweepDirection);
} }
public override string ToString()
=> $"A {Size} {RotationAngle.ToString(CultureInfo.InvariantCulture)} {(IsLargeArc ? 1 : 0)} {(int)SweepDirection} {Point}";
} }
} }

3
src/Avalonia.Visuals/Media/BezierSegment .cs

@ -61,5 +61,8 @@ namespace Avalonia.Media
{ {
ctx.CubicBezierTo(Point1, Point2, Point3); ctx.CubicBezierTo(Point1, Point2, Point3);
} }
public override string ToString()
=> $"C {Point1} {Point2} {Point3}";
} }
} }

3
src/Avalonia.Visuals/Media/LineSegment.cs

@ -27,5 +27,8 @@ namespace Avalonia.Media
{ {
ctx.LineTo(Point); ctx.LineTo(Point);
} }
public override string ToString()
=> $"L {Point}";
} }
} }

3
src/Avalonia.Visuals/Media/PathFigure.cs

@ -98,5 +98,8 @@ namespace Avalonia.Media
} }
private PathSegments _segments; private PathSegments _segments;
public override string ToString()
=> $"M {StartPoint} {string.Join(" ", _segments)}{(IsClosed ? "Z" : "")}";
} }
} }

4
src/Avalonia.Visuals/Media/PathGeometry.cs

@ -112,5 +112,9 @@ namespace Avalonia.Media
() => InvalidateGeometry()); () => InvalidateGeometry());
_figuresPropertiesObserver = figures?.TrackItemPropertyChanged(_ => InvalidateGeometry()); _figuresPropertiesObserver = figures?.TrackItemPropertyChanged(_ => InvalidateGeometry());
} }
public override string ToString()
=> $"{(FillRule != FillRule.EvenOdd ? "F1 " : "")}{(string.Join(" ", Figures))}";
} }
} }

3
src/Avalonia.Visuals/Media/QuadraticBezierSegment .cs

@ -42,5 +42,8 @@ namespace Avalonia.Media
{ {
ctx.QuadraticBezierTo(Point1, Point2); ctx.QuadraticBezierTo(Point1, Point2);
} }
public override string ToString()
=> $"Q {Point1} {Point2}";
} }
} }

26
src/Avalonia.Visuals/Visual.cs

@ -386,11 +386,18 @@ namespace Avalonia
AttachedToVisualTree?.Invoke(this, e); AttachedToVisualTree?.Invoke(this, e);
InvalidateVisual(); InvalidateVisual();
if (VisualChildren != null) var visualChildren = VisualChildren;
if (visualChildren != null)
{ {
foreach (Visual child in VisualChildren.OfType<Visual>()) var visualChildrenCount = visualChildren.Count;
for (var i = 0; i < visualChildrenCount; i++)
{ {
child.OnAttachedToVisualTreeCore(e); if (visualChildren[i] is Visual child)
{
child.OnAttachedToVisualTreeCore(e);
}
} }
} }
} }
@ -415,11 +422,18 @@ namespace Avalonia
DetachedFromVisualTree?.Invoke(this, e); DetachedFromVisualTree?.Invoke(this, e);
e.Root?.Renderer?.AddDirty(this); e.Root?.Renderer?.AddDirty(this);
if (VisualChildren != null) var visualChildren = VisualChildren;
if (visualChildren != null)
{ {
foreach (Visual child in VisualChildren.OfType<Visual>()) var visualChildrenCount = visualChildren.Count;
for (var i = 0; i < visualChildrenCount; i++)
{ {
child.OnDetachedFromVisualTreeCore(e); if (visualChildren[i] is Visual child)
{
child.OnDetachedFromVisualTreeCore(e);
}
} }
} }
} }

17
src/Avalonia.X11/X11Window.cs

@ -995,11 +995,18 @@ namespace Avalonia.X11
public void SetIcon(IWindowIconImpl icon) public void SetIcon(IWindowIconImpl icon)
{ {
var data = ((X11IconData)icon).Data; if (icon != null)
fixed (void* pdata = data) {
XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_ICON, var data = ((X11IconData)icon).Data;
new IntPtr((int)Atom.XA_CARDINAL), 32, PropertyMode.Replace, fixed (void* pdata = data)
pdata, data.Length); XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_ICON,
new IntPtr((int)Atom.XA_CARDINAL), 32, PropertyMode.Replace,
pdata, data.Length);
}
else
{
XDeleteProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_ICON);
}
} }
public void ShowTaskbarIcon(bool value) public void ShowTaskbarIcon(bool value)

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

@ -942,7 +942,7 @@ namespace Avalonia.Win32
public void SetIcon(IWindowIconImpl icon) public void SetIcon(IWindowIconImpl icon)
{ {
var impl = (IconImpl)icon; var impl = (IconImpl)icon;
var hIcon = impl.HIcon; var hIcon = impl?.HIcon ?? IntPtr.Zero;
UnmanagedMethods.PostMessage(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_SETICON, UnmanagedMethods.PostMessage(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_SETICON,
new IntPtr((int)UnmanagedMethods.Icons.ICON_BIG), hIcon); new IntPtr((int)UnmanagedMethods.Icons.ICON_BIG), hIcon);
} }

17
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs

@ -16,19 +16,6 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("foodefault", target.GetValue(Class2.FooProperty)); Assert.Equal("foodefault", target.GetValue(Class2.FooProperty));
} }
[Fact]
public void AvaloniaProperty_Initialized_Is_Called_For_Attached_Property()
{
bool raised = false;
using (Class1.FooProperty.Initialized.Subscribe(x => raised = true))
{
new Class3();
}
Assert.True(raised);
}
private class Base : AvaloniaObject private class Base : AvaloniaObject
{ {
} }
@ -46,9 +33,5 @@ namespace Avalonia.Base.UnitTests
public static readonly AttachedProperty<string> FooProperty = public static readonly AttachedProperty<string> FooProperty =
Class1.FooProperty.AddOwner<Class2>(); Class1.FooProperty.AddOwner<Class2>();
} }
private class Class3 : Base
{
}
} }
} }

105
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@ -132,6 +132,111 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("foo", target.GetValue(property)); Assert.Equal("foo", target.GetValue(property));
} }
[Fact]
public void Completing_LocalValue_Binding_Raises_PropertyChanged()
{
var target = new Class1();
var source = new BehaviorSubject<BindingValue<string>>("foo");
var property = Class1.FooProperty;
var raised = 0;
target.Bind(property, source);
Assert.Equal("foo", target.GetValue(property));
target.PropertyChanged += (s, e) =>
{
Assert.Equal(BindingPriority.Unset, e.Priority);
Assert.Equal(property, e.Property);
Assert.Equal("foo", e.OldValue as string);
Assert.Equal("foodefault", e.NewValue as string);
++raised;
};
source.OnCompleted();
Assert.Equal("foodefault", target.GetValue(property));
Assert.Equal(1, raised);
}
[Fact]
public void Completing_Style_Binding_Raises_PropertyChanged()
{
var target = new Class1();
var source = new BehaviorSubject<BindingValue<string>>("foo");
var property = Class1.FooProperty;
var raised = 0;
target.Bind(property, source, BindingPriority.Style);
Assert.Equal("foo", target.GetValue(property));
target.PropertyChanged += (s, e) =>
{
Assert.Equal(BindingPriority.Unset, e.Priority);
Assert.Equal(property, e.Property);
Assert.Equal("foo", e.OldValue as string);
Assert.Equal("foodefault", e.NewValue as string);
++raised;
};
source.OnCompleted();
Assert.Equal("foodefault", target.GetValue(property));
Assert.Equal(1, raised);
}
[Fact]
public void Completing_LocalValue_Binding_With_Style_Binding_Raises_PropertyChanged()
{
var target = new Class1();
var source = new BehaviorSubject<BindingValue<string>>("foo");
var property = Class1.FooProperty;
var raised = 0;
target.Bind(property, new BehaviorSubject<string>("bar"), BindingPriority.Style);
target.Bind(property, source);
Assert.Equal("foo", target.GetValue(property));
target.PropertyChanged += (s, e) =>
{
Assert.Equal(BindingPriority.Style, e.Priority);
Assert.Equal(property, e.Property);
Assert.Equal("foo", e.OldValue as string);
Assert.Equal("bar", e.NewValue as string);
++raised;
};
source.OnCompleted();
Assert.Equal("bar", target.GetValue(property));
Assert.Equal(1, raised);
}
[Fact]
public void Disposing_LocalValue_Binding_Raises_PropertyChanged()
{
var target = new Class1();
var source = new BehaviorSubject<BindingValue<string>>("foo");
var property = Class1.FooProperty;
var raised = 0;
var sub = target.Bind(property, source);
Assert.Equal("foo", target.GetValue(property));
target.PropertyChanged += (s, e) =>
{
Assert.Equal(BindingPriority.Unset, e.Priority);
Assert.Equal(property, e.Property);
Assert.Equal("foo", e.OldValue as string);
Assert.Equal("foodefault", e.NewValue as string);
++raised;
};
sub.Dispose();
Assert.Equal("foodefault", target.GetValue(property));
Assert.Equal(1, raised);
}
[Fact] [Fact]
public void Setting_Style_Value_Overrides_Binding_Permanently() public void Setting_Style_Value_Overrides_Binding_Permanently()
{ {

17
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs

@ -188,6 +188,7 @@ namespace Avalonia.Base.UnitTests
target.PropertyChanged += (s, e) => target.PropertyChanged += (s, e) =>
{ {
Assert.Same(target, s); Assert.Same(target, s);
Assert.Equal(BindingPriority.LocalValue, e.Priority);
Assert.Equal(Class1.FooProperty, e.Property); Assert.Equal(Class1.FooProperty, e.Property);
Assert.Equal("newvalue", (string)e.OldValue); Assert.Equal("newvalue", (string)e.OldValue);
Assert.Equal("unset", (string)e.NewValue); Assert.Equal("unset", (string)e.NewValue);
@ -424,22 +425,6 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("second", target.Foo); Assert.Equal("second", target.Foo);
} }
[Fact]
public void Property_Notifies_Initialized()
{
bool raised = false;
Class1.FooProperty.Initialized.Subscribe(e =>
raised = e.Property == Class1.FooProperty &&
e.OldValue == AvaloniaProperty.UnsetValue &&
(string)e.NewValue == "initial" &&
e.Priority == BindingPriority.Unset);
var target = new Class1();
Assert.True(raised);
}
[Fact] [Fact]
public void Binding_Error_Reverts_To_Default_Value() public void Binding_Error_Reverts_To_Default_Value()
{ {

12
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs

@ -30,6 +30,7 @@ namespace Avalonia.Base.UnitTests
target.PropertyChanged += (s, e) => target.PropertyChanged += (s, e) =>
{ {
Assert.Same(target, s); Assert.Same(target, s);
Assert.Equal(BindingPriority.Unset, e.Priority);
Assert.Equal(Class1.FooProperty, e.Property); Assert.Equal(Class1.FooProperty, e.Property);
Assert.Equal("newvalue", (string)e.OldValue); Assert.Equal("newvalue", (string)e.OldValue);
Assert.Equal("foodefault", (string)e.NewValue); Assert.Equal("foodefault", (string)e.NewValue);
@ -239,6 +240,17 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("two", target.GetValue(Class1.FooProperty)); Assert.Equal("two", target.GetValue(Class1.FooProperty));
} }
[Fact]
public void SetValue_Animation_Overrides_LocalValue()
{
Class1 target = new Class1();
target.SetValue(Class1.FooProperty, "one", BindingPriority.LocalValue);
Assert.Equal("one", target.GetValue(Class1.FooProperty));
target.SetValue(Class1.FooProperty, "two", BindingPriority.Animation);
Assert.Equal("two", target.GetValue(Class1.FooProperty));
}
[Fact] [Fact]
public void Setting_UnsetValue_Reverts_To_Default_Value() public void Setting_UnsetValue_Reverts_To_Default_Value()
{ {

23
tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs

@ -78,24 +78,6 @@ namespace Avalonia.Base.UnitTests
Assert.Equal(BindingMode.TwoWay, result.DefaultBindingMode); Assert.Equal(BindingMode.TwoWay, result.DefaultBindingMode);
} }
[Fact]
public void Initialized_Observable_Fired()
{
bool invoked = false;
Class1.FooProperty.Initialized.Subscribe(x =>
{
Assert.Equal(AvaloniaProperty.UnsetValue, x.OldValue);
Assert.Equal("default", x.NewValue);
Assert.Equal(BindingPriority.Unset, x.Priority);
invoked = true;
});
var target = new Class1();
Assert.True(invoked);
}
[Fact] [Fact]
public void Changed_Observable_Fired() public void Changed_Observable_Fired()
{ {
@ -141,11 +123,6 @@ namespace Avalonia.Base.UnitTests
OverrideMetadata(typeof(T), metadata); OverrideMetadata(typeof(T), metadata);
} }
internal override void NotifyInitialized(IAvaloniaObject o)
{
throw new NotImplementedException();
}
internal override IDisposable RouteBind( internal override IDisposable RouteBind(
IAvaloniaObject o, IAvaloniaObject o,
IObservable<BindingValue<object>> source, IObservable<BindingValue<object>> source,

12
tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs

@ -50,6 +50,18 @@ namespace Avalonia.Base.UnitTests.Data.Converters
Assert.Equal(TestEnum.Bar, result); Assert.Equal(TestEnum.Bar, result);
} }
[Fact]
public void Can_Convert_String_To_TimeSpan()
{
var result = DefaultValueConverter.Instance.Convert(
"00:00:10",
typeof(TimeSpan),
null,
CultureInfo.InvariantCulture);
Assert.Equal(TimeSpan.FromSeconds(10), result);
}
[Fact] [Fact]
public void Can_Convert_Int_To_Enum() public void Can_Convert_Int_To_Enum()
{ {

22
tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs

@ -1,33 +1,12 @@
// 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 System.Reactive.Subjects;
using Avalonia.Data;
using Xunit; using Xunit;
namespace Avalonia.Base.UnitTests namespace Avalonia.Base.UnitTests
{ {
public class DirectPropertyTests public class DirectPropertyTests
{ {
[Fact]
public void Initialized_Observable_Fired()
{
bool invoked = false;
Class1.FooProperty.Initialized.Subscribe(x =>
{
Assert.Equal(AvaloniaProperty.UnsetValue, x.OldValue);
Assert.Equal("foo", x.NewValue);
Assert.Equal(BindingPriority.Unset, x.Priority);
invoked = true;
});
var target = new Class1();
Assert.True(invoked);
}
[Fact] [Fact]
public void IsDirect_Property_Returns_True() public void IsDirect_Property_Returns_True()
{ {
@ -69,7 +48,6 @@ namespace Avalonia.Base.UnitTests
var p2 = p1.AddOwner<Class2>(o => null, (o, v) => { }); var p2 = p1.AddOwner<Class2>(o => null, (o, v) => { });
Assert.Same(p1.Changed, p2.Changed); Assert.Same(p1.Changed, p2.Changed);
Assert.Same(p1.Initialized, p2.Initialized);
} }
private class Class1 : AvaloniaObject private class Class1 : AvaloniaObject

2
tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs

@ -6,7 +6,7 @@ namespace Avalonia.Benchmarks.Base
[MemoryDiagnoser] [MemoryDiagnoser]
public class AvaloniaObjectInitializationBenchmark public class AvaloniaObjectInitializationBenchmark
{ {
[Benchmark(OperationsPerInvoke = 1000)] [Benchmark]
public Button InitializeButton() public Button InitializeButton()
{ {
return new Button(); return new Button();

46
tests/Avalonia.Benchmarks/Layout/CalendarBenchmark.cs

@ -0,0 +1,46 @@
using System;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Layout
{
[MemoryDiagnoser]
public class CalendarBenchmark : IDisposable
{
private readonly IDisposable _app;
private readonly TestRoot _root;
public CalendarBenchmark()
{
_app = UnitTestApplication.Start(
TestServices.StyledWindow.With(
renderInterface: new NullRenderingPlatform(),
threadingInterface: new NullThreadingPlatform()));
_root = new TestRoot(true, null)
{
Renderer = new NullRenderer()
};
_root.LayoutManager.ExecuteInitialLayoutPass(_root);
}
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public void CreateCalendar()
{
var calendar = new Calendar();
_root.Child = calendar;
_root.LayoutManager.ExecuteLayoutPass();
}
public void Dispose()
{
_app.Dispose();
}
}
}

36
tests/Avalonia.Benchmarks/NullFormattedTextImpl.cs

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Platform;
namespace Avalonia.Benchmarks
{
internal class NullFormattedTextImpl : IFormattedTextImpl
{
public Size Constraint { get; }
public Rect Bounds { get; }
public string Text { get; }
public IEnumerable<FormattedTextLine> GetLines()
{
throw new NotImplementedException();
}
public TextHitTestResult HitTestPoint(Point point)
{
throw new NotImplementedException();
}
public Rect HitTestTextPosition(int index)
{
throw new NotImplementedException();
}
public IEnumerable<Rect> HitTestTextRange(int index, int length)
{
throw new NotImplementedException();
}
}
}

78
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.IO;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
namespace Avalonia.Benchmarks
{
internal class NullRenderingPlatform : IPlatformRenderInterface
{
public IFormattedTextImpl CreateFormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment,
TextWrapping wrapping, Size constraint, IReadOnlyList<FormattedTextStyleSpan> spans)
{
return new NullFormattedTextImpl();
}
public IGeometryImpl CreateEllipseGeometry(Rect rect)
{
throw new NotImplementedException();
}
public IGeometryImpl CreateLineGeometry(Point p1, Point p2)
{
throw new NotImplementedException();
}
public IGeometryImpl CreateRectangleGeometry(Rect rect)
{
throw new NotImplementedException();
}
public IStreamGeometryImpl CreateStreamGeometry()
{
return new MockStreamGeometryImpl();
}
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
{
throw new NotImplementedException();
}
public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi)
{
throw new NotImplementedException();
}
public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? format = null)
{
throw new NotImplementedException();
}
public IBitmapImpl LoadBitmap(string fileName)
{
throw new NotImplementedException();
}
public IBitmapImpl LoadBitmap(Stream stream)
{
throw new NotImplementedException();
}
public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride)
{
throw new NotImplementedException();
}
public IFontManagerImpl CreateFontManager()
{
return new MockFontManagerImpl();
}
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{
throw new NotImplementedException();
}
}
}

28
tests/Avalonia.Benchmarks/NullThreadingPlatform.cs

@ -0,0 +1,28 @@
using System;
using System.Reactive.Disposables;
using System.Threading;
using Avalonia.Platform;
using Avalonia.Threading;
namespace Avalonia.Benchmarks
{
internal class NullThreadingPlatform : IPlatformThreadingInterface
{
public void RunLoop(CancellationToken cancellationToken)
{
}
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
{
return Disposable.Empty;
}
public void Signal(DispatcherPriority priority)
{
}
public bool CurrentThreadIsLoopThread => true;
public event Action<DispatcherPriority?> Signaled;
}
}

47
tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs

@ -0,0 +1,47 @@
using System;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Styling
{
[MemoryDiagnoser]
public class StyleAttachBenchmark : IDisposable
{
private readonly IDisposable _app;
private readonly TestRoot _root;
private readonly TextBox _control;
public StyleAttachBenchmark()
{
_app = UnitTestApplication.Start(
TestServices.StyledWindow.With(
renderInterface: new NullRenderingPlatform(),
threadingInterface: new NullThreadingPlatform()));
_root = new TestRoot(true, null)
{
Renderer = new NullRenderer(),
};
_control = new TextBox();
}
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public void AttachTextBoxStyles()
{
var styles = UnitTestApplication.Current.Styles;
styles.Attach(_control, UnitTestApplication.Current);
styles.Detach();
}
public void Dispose()
{
_app.Dispose();
}
}
}

46
tests/Avalonia.Controls.UnitTests/CarouselTests.cs

@ -51,10 +51,9 @@ namespace Avalonia.Controls.UnitTests
Assert.Single(target.GetLogicalChildren()); Assert.Single(target.GetLogicalChildren());
var child = target.GetLogicalChildren().Single(); var child = GetContainerTextBlock(target.GetLogicalChildren().Single());
Assert.IsType<TextBlock>(child); Assert.Equal("Foo", child.Text);
Assert.Equal("Foo", ((TextBlock)child).Text);
} }
[Fact] [Fact]
@ -98,20 +97,18 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(3, target.GetLogicalChildren().Count()); Assert.Equal(3, target.GetLogicalChildren().Count());
var child = target.GetLogicalChildren().First(); var child = GetContainerTextBlock(target.GetLogicalChildren().First());
Assert.IsType<TextBlock>(child); Assert.Equal("Foo", child.Text);
Assert.Equal("Foo", ((TextBlock)child).Text);
var newItems = items.ToList(); var newItems = items.ToList();
newItems.RemoveAt(0); newItems.RemoveAt(0);
target.Items = newItems; target.Items = newItems;
child = target.GetLogicalChildren().First(); child = GetContainerTextBlock(target.GetLogicalChildren().First());
Assert.IsType<TextBlock>(child); Assert.Equal("Bar", child.Text);
Assert.Equal("Bar", ((TextBlock)child).Text);
} }
[Fact] [Fact]
@ -136,20 +133,18 @@ namespace Avalonia.Controls.UnitTests
Assert.Single(target.GetLogicalChildren()); Assert.Single(target.GetLogicalChildren());
var child = target.GetLogicalChildren().Single(); var child = GetContainerTextBlock(target.GetLogicalChildren().Single());
Assert.IsType<TextBlock>(child); Assert.Equal("Foo", child.Text);
Assert.Equal("Foo", ((TextBlock)child).Text);
var newItems = items.ToList(); var newItems = items.ToList();
newItems.RemoveAt(0); newItems.RemoveAt(0);
target.Items = newItems; target.Items = newItems;
child = target.GetLogicalChildren().Single(); child = GetContainerTextBlock(target.GetLogicalChildren().Single());
Assert.IsType<TextBlock>(child); Assert.Equal("Bar", child.Text);
Assert.Equal("Bar", ((TextBlock)child).Text);
} }
[Fact] [Fact]
@ -197,10 +192,9 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(3, target.GetLogicalChildren().Count()); Assert.Equal(3, target.GetLogicalChildren().Count());
var child = target.GetLogicalChildren().First(); var child = GetContainerTextBlock(target.GetLogicalChildren().First());
Assert.IsType<TextBlock>(child); Assert.Equal("Foo", child.Text);
Assert.Equal("Foo", ((TextBlock)child).Text);
target.Items = null; target.Items = null;
@ -233,7 +227,7 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal("FooBar", target.SelectedItem); Assert.Equal("FooBar", target.SelectedItem);
var child = target.GetVisualDescendants().LastOrDefault(); var child = GetContainerTextBlock(target.GetVisualDescendants().LastOrDefault());
Assert.IsType<TextBlock>(child); Assert.IsType<TextBlock>(child);
Assert.Equal("FooBar", ((TextBlock)child).Text); Assert.Equal("FooBar", ((TextBlock)child).Text);
@ -261,14 +255,13 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(3, target.GetLogicalChildren().Count()); Assert.Equal(3, target.GetLogicalChildren().Count());
var child = target.GetLogicalChildren().First(); var child = GetContainerTextBlock(target.GetLogicalChildren().First());
Assert.IsType<TextBlock>(child); Assert.Equal("Foo", child.Text);
Assert.Equal("Foo", ((TextBlock)child).Text);
items.RemoveAt(0); items.RemoveAt(0);
child = target.GetLogicalChildren().First(); child = GetContainerTextBlock(target.GetLogicalChildren().First());
Assert.IsType<TextBlock>(child); Assert.IsType<TextBlock>(child);
Assert.Equal("Bar", ((TextBlock)child).Text); Assert.Equal("Bar", ((TextBlock)child).Text);
@ -314,5 +307,12 @@ namespace Avalonia.Controls.UnitTests
[~CarouselPresenter.PageTransitionProperty] = control[~Carousel.PageTransitionProperty], [~CarouselPresenter.PageTransitionProperty] = control[~Carousel.PageTransitionProperty],
}.RegisterInNameScope(scope); }.RegisterInNameScope(scope);
} }
private static TextBlock GetContainerTextBlock(object control)
{
var contentPresenter = Assert.IsType<ContentPresenter>(control);
contentPresenter.UpdateChild();
return Assert.IsType<TextBlock>(contentPresenter.Child);
}
} }
} }

31
tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

@ -108,5 +108,36 @@ namespace Avalonia.Controls.UnitTests
}; };
}); });
} }
[Fact]
public void Detaching_Closed_ComboBox_Keeps_Current_Focus()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target = new ComboBox
{
Items = new[] { new Canvas() },
SelectedIndex = 0,
Template = GetTemplate(),
};
var other = new Control { Focusable = true };
StackPanel panel;
var root = new TestRoot { Child = panel = new StackPanel { Children = { target, other } } };
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
other.Focus();
Assert.True(other.IsFocused);
panel.Children.Remove(target);
Assert.True(other.IsFocused);
}
}
} }
} }

32
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@ -66,7 +66,7 @@ namespace Avalonia.Controls.UnitTests
} }
[Fact] [Fact]
public void Container_Child_Should_Have_LogicalParent_Set_To_Container() public void Container_Should_Have_LogicalParent_Set_To_ItemsControl()
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
@ -87,7 +87,7 @@ namespace Avalonia.Controls.UnitTests
var container = (ContentPresenter)target.Presenter.Panel.Children[0]; var container = (ContentPresenter)target.Presenter.Panel.Children[0];
Assert.Equal(container, container.Child.Parent); Assert.Equal(target, container.Parent);
} }
} }
@ -190,7 +190,7 @@ namespace Avalonia.Controls.UnitTests
} }
[Fact] [Fact]
public void Adding_String_Item_Should_Make_TextBlock_Appear_In_LogicalChildren() public void Adding_String_Item_Should_Make_ContentPresenter_Appear_In_LogicalChildren()
{ {
var target = new ItemsControl(); var target = new ItemsControl();
var child = new Control(); var child = new Control();
@ -202,7 +202,7 @@ namespace Avalonia.Controls.UnitTests
var logical = (ILogical)target; var logical = (ILogical)target;
Assert.Equal(1, logical.LogicalChildren.Count); Assert.Equal(1, logical.LogicalChildren.Count);
Assert.IsType<TextBlock>(logical.LogicalChildren[0]); Assert.IsType<ContentPresenter>(logical.LogicalChildren[0]);
} }
[Fact] [Fact]
@ -575,6 +575,30 @@ namespace Avalonia.Controls.UnitTests
}; };
} }
[Fact]
public void Detaching_Then_Reattaching_To_Logical_Tree_Twice_Does_Not_Throw()
{
// # Issue 3487
var target = new ItemsControl
{
Template = GetTemplate(),
Items = new[] { "foo", "bar" },
ItemTemplate = new FuncDataTemplate<string>((_, __) => new Canvas()),
};
var root = new TestRoot(target);
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
root.Child = null;
root.Child = target;
target.Measure(Size.Infinity);
root.Child = null;
root.Child = target;
}
private class Item private class Item
{ {
public Item(string value) public Item(string value)

28
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@ -223,7 +223,33 @@ namespace Avalonia.Controls.UnitTests.Primitives
} }
} }
[Fact]
public void Popup_Close_On_Closed_Popup_Should_Not_Raise_Closed_Event()
{
using (CreateServices())
{
var window = PreparedWindow();
var target = new Popup() { PlacementMode = PlacementMode.Pointer };
window.Content = target;
window.ApplyTemplate();
int closedCount = 0;
target.Closed += (sender, args) =>
{
closedCount++;
};
target.Close();
target.Close();
target.Close();
target.Close();
Assert.Equal(0, closedCount);
}
}
[Fact] [Fact]
public void Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent() public void Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent()
{ {

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

@ -1202,6 +1202,28 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.MoveSelection(NavigationDirection.Next, true); target.MoveSelection(NavigationDirection.Next, true);
} }
[Fact]
public void MoveSelection_Does_Select_Disabled_Controls()
{
// Issue #3426.
var target = new TestSelector
{
Template = Template(),
Items = new[]
{
new ListBoxItem(),
new ListBoxItem { IsEnabled = false },
},
SelectedIndex = 0,
};
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
target.MoveSelection(NavigationDirection.Next, true);
Assert.Equal(0, target.SelectedIndex);
}
[Fact] [Fact]
public void Pre_Selecting_Item_Should_Set_Selection_After_It_Was_Added_When_AlwaysSelected() public void Pre_Selecting_Item_Should_Set_Selection_After_It_Was_Added_When_AlwaysSelected()
{ {

24
tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs

@ -65,6 +65,30 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Vector(10, 10), target.Offset); Assert.Equal(new Vector(10, 10), target.Offset);
} }
[Fact]
public void Test_ScrollToHome()
{
var target = new ScrollViewer();
target.SetValue(ScrollViewer.ExtentProperty, new Size(50, 50));
target.SetValue(ScrollViewer.ViewportProperty, new Size(10, 10));
target.Offset = new Vector(25, 25);
target.ScrollToHome();
Assert.Equal(new Vector(0, 0), target.Offset);
}
[Fact]
public void Test_ScrollToEnd()
{
var target = new ScrollViewer();
target.SetValue(ScrollViewer.ExtentProperty, new Size(50, 50));
target.SetValue(ScrollViewer.ViewportProperty, new Size(10, 10));
target.Offset = new Vector(25, 25);
target.ScrollToEnd();
Assert.Equal(new Vector(0, 40), target.Offset);
}
private Control CreateTemplate(ScrollViewer control, INameScope scope) private Control CreateTemplate(ScrollViewer control, INameScope scope)
{ {
return new Grid return new Grid

51
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -341,11 +341,62 @@ namespace Avalonia.Controls.UnitTests
} }
} }
[Fact]
public void Child_Should_Be_Measured_With_Width_And_Height_If_SizeToContent_Is_Manual()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new ChildControl();
var target = new Window
{
Width = 100,
Height = 50,
SizeToContent = SizeToContent.Manual,
Content = child
};
target.Show();
Assert.Equal(new Size(100, 50), child.MeasureSize);
}
}
[Fact]
public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new ChildControl();
var target = new Window
{
Width = 100,
Height = 50,
SizeToContent = SizeToContent.WidthAndHeight,
Content = child
};
target.Show();
Assert.Equal(Size.Infinity, child.MeasureSize);
}
}
private IWindowImpl CreateImpl(Mock<IRenderer> renderer) private IWindowImpl CreateImpl(Mock<IRenderer> renderer)
{ {
return Mock.Of<IWindowImpl>(x => return Mock.Of<IWindowImpl>(x =>
x.Scaling == 1 && x.Scaling == 1 &&
x.CreateRenderer(It.IsAny<IRenderRoot>()) == renderer.Object); x.CreateRenderer(It.IsAny<IRenderRoot>()) == renderer.Object);
} }
private class ChildControl : Control
{
public Size MeasureSize { get; private set; }
protected override Size MeasureOverride(Size availableSize)
{
MeasureSize = availableSize;
return base.MeasureOverride(availableSize);
}
}
} }
} }

33
tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs

@ -374,5 +374,38 @@ namespace Avalonia.Layout.UnitTests
Assert.True(control.Measured); Assert.True(control.Measured);
Assert.True(control.IsMeasureValid); Assert.True(control.IsMeasureValid);
} }
[Fact]
public void Calling_ExecuteLayoutPass_From_ExecuteInitialLayoutPass_Does_Not_Break_Measure()
{
// Test for issue #3550.
var control = new LayoutTestControl();
var root = new LayoutTestRoot { Child = control };
var count = 0;
root.LayoutManager.ExecuteInitialLayoutPass(root);
control.Measured = false;
control.DoMeasureOverride = (l, s) =>
{
if (count++ == 0)
{
control.InvalidateMeasure();
root.LayoutManager.ExecuteLayoutPass();
return new Size(100, 100);
}
else
{
return new Size(200, 200);
}
};
root.InvalidateMeasure();
control.InvalidateMeasure();
root.LayoutManager.ExecuteInitialLayoutPass(root);
Assert.Equal(new Size(200, 200), control.Bounds.Size);
Assert.Equal(new Size(200, 200), control.DesiredSize);
}
} }
} }

52
tests/Avalonia.LeakTests/ControlTests.cs

@ -8,8 +8,10 @@ using Avalonia.Controls;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Diagnostics; using Avalonia.Diagnostics;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using JetBrains.dotMemoryUnit; using JetBrains.dotMemoryUnit;
@ -370,6 +372,56 @@ namespace Avalonia.LeakTests
} }
} }
[Fact]
public void Control_With_Style_RenderTransform_Is_Freed()
{
// # Issue #3545
using (Start())
{
Func<Window> run = () =>
{
var window = new Window
{
Styles =
{
new Style(x => x.OfType<Canvas>())
{
Setters =
{
new Setter
{
Property = Visual.RenderTransformProperty,
Value = new RotateTransform(45),
}
}
}
},
Content = new Canvas()
};
window.Show();
// Do a layout and make sure that Canvas gets added to visual tree with
// its render transform.
window.LayoutManager.ExecuteInitialLayoutPass(window);
var canvas = Assert.IsType<Canvas>(window.Presenter.Child);
Assert.IsType<RotateTransform>(canvas.RenderTransform);
// Clear the content and ensure the Canvas is removed.
window.Content = null;
window.LayoutManager.ExecuteLayoutPass();
Assert.Null(window.Presenter.Child);
return window;
};
var result = run();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Canvas>()).ObjectsCount));
}
}
private IDisposable Start() private IDisposable Start()
{ {
return UnitTestApplication.Start(TestServices.StyledWindow); return UnitTestApplication.Start(TestServices.StyledWindow);

42
tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs

@ -221,6 +221,48 @@ namespace Avalonia.Visuals.UnitTests.Media
context.Verify(v => v.EndFigure(It.IsAny<bool>()), Times.AtLeastOnce()); context.Verify(v => v.EndFigure(It.IsAny<bool>()), Times.AtLeastOnce());
} }
[Theory]
[InlineData("M 5.5, 5 L 5.5, 5 L 5.5, 5")]
[InlineData("F1 M 9.0771, 11 C 9.1161, 10.701 9.1801, 10.352 9.3031, 10 L 9.0001, 10 L 9.0001, 6.166 L 3.0001, 9.767 L 3.0001, 10 "
+ "L 9.99999999997669E-05, 10 L 9.99999999997669E-05, 0 L 3.0001, 0 L 3.0001, 0.234 L 9.0001, 3.834 L 9.0001, 0 "
+ "L 12.0001, 0 L 12.0001, 8.062 C 12.1861, 8.043 12.3821, 8.031 12.5941, 8.031 C 15.3481, 8.031 15.7961, 9.826 "
+ "15.9201, 11 L 16.0001, 16 L 9.0001, 16 L 9.0001, 12.562 L 9.0001, 11Z")]
[InlineData("F1 M 24, 14 A 2, 2 0 1 1 20, 14 A 2, 2 0 1 1 24, 14Z")]
[InlineData("M 0, 0 L 10, 10Z")]
[InlineData("M 50, 50 L 100, 100 L 150, 50")]
[InlineData("M 50, 50 L -10, -10 L 10, 50")]
[InlineData("M 50, 50 L 100, 100 L 150, 50Z M 50, 50 L 70, 70 L 120, 50Z")]
[InlineData("M 80, 200 A 100, 50 45 1 0 100, 50")]
[InlineData("F1 M 16, 12 C 16, 14.209 14.209, 16 12, 16 C 9.791, 16 8, 14.209 8, 12 C 8, 11.817 8.03, 11.644 8.054, 11.467 L 6.585, 10 "
+ "L 4, 10 L 4, 6.414 L 2.5, 7.914 L 0, 5.414 L 0, 3.586 L 3.586, 0 L 4.414, 0 L 7.414, 3 L 7.586, 3 L 9, 1.586 L "
+ "11.914, 4.5 L 10.414, 6 L 12.461, 8.046 C 14.45, 8.278 16, 9.949 16, 12")]
public void Parsed_Geometry_ToString_Should_Produce_Valid_Value(string pathData)
{
var target = PathGeometry.Parse(pathData);
string output = target.ToString();
Assert.Equal(pathData, output);
}
[Theory]
[InlineData("M5.5.5 5.5.5 5.5.5", "M 5.5, 0.5 L 5.5, 0.5 L 5.5, 0.5")]
[InlineData("F1 M24,14 A2,2,0,1,1,20,14 A2,2,0,1,1,24,14 z", "F1 M 24, 14 A 2, 2 0 1 1 20, 14 A 2, 2 0 1 1 24, 14Z")]
[InlineData("F1M16,12C16,14.209 14.209,16 12,16 9.791,16 8,14.209 8,12 8,11.817 8.03,11.644 8.054,11.467L6.585,10 4,10 "
+ "4,6.414 2.5,7.914 0,5.414 0,3.586 3.586,0 4.414,0 7.414,3 7.586,3 9,1.586 11.914,4.5 10.414,6 "
+ "12.461,8.046C14.45,8.278,16,9.949,16,12",
"F1 M 16, 12 C 16, 14.209 14.209, 16 12, 16 C 9.791, 16 8, 14.209 8, 12 C 8, 11.817 8.03, 11.644 8.054, 11.467 L 6.585, 10 "
+ "L 4, 10 L 4, 6.414 L 2.5, 7.914 L 0, 5.414 L 0, 3.586 L 3.586, 0 L 4.414, 0 L 7.414, 3 L 7.586, 3 L 9, 1.586 L "
+ "11.914, 4.5 L 10.414, 6 L 12.461, 8.046 C 14.45, 8.278 16, 9.949 16, 12")]
public void Parsed_Geometry_ToString_Should_Format_Value(string pathData, string formattedPathData)
{
var target = PathGeometry.Parse(pathData);
string output = target.ToString();
Assert.Equal(formattedPathData, output);
}
[Theory] [Theory]
[InlineData("0 0")] [InlineData("0 0")]
[InlineData("j")] [InlineData("j")]

Loading…
Cancel
Save