Browse Source

Merge branch 'master' into comboboxHeightFix

pull/3528/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
0a28b99690
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      readme.md
  2. 1
      src/Avalonia.Base/AvaloniaObject.cs
  3. 39
      src/Avalonia.Base/AvaloniaProperty.cs
  4. 45
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  5. 15
      src/Avalonia.Base/DirectPropertyBase.cs
  6. 15
      src/Avalonia.Base/StyledPropertyBase.cs
  7. 9
      src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs
  8. 8
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  9. 4
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  10. 21
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  11. 2
      src/Avalonia.Controls/AutoCompleteBox.cs
  12. 25
      src/Avalonia.Controls/Button.cs
  13. 28
      src/Avalonia.Controls/ButtonSpinner.cs
  14. 12
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  15. 2
      src/Avalonia.Controls/Calendar/DatePicker.cs
  16. 4
      src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs
  17. 20
      src/Avalonia.Controls/ControlExtensions.cs
  18. 41
      src/Avalonia.Controls/Expander.cs
  19. 39
      src/Avalonia.Controls/ItemsControl.cs
  20. 30
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  21. 283
      src/Avalonia.Controls/Primitives/Popup.cs
  22. 24
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  23. 8
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  24. 24
      src/Avalonia.Controls/Primitives/ToggleButton.cs
  25. 28
      src/Avalonia.Controls/Primitives/Track.cs
  26. 44
      src/Avalonia.Controls/ProgressBar.cs
  27. 4
      src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs
  28. 16
      src/Avalonia.Controls/ScrollViewer.cs
  29. 6
      src/Avalonia.Controls/SelectionChangedEventArgs.cs
  30. 24
      src/Avalonia.Controls/Slider.cs
  31. 4
      src/Avalonia.Controls/TreeView.cs
  32. 42
      src/Avalonia.Input/InputElement.cs
  33. 12
      src/Avalonia.Layout/LayoutManager.cs
  34. 28
      src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs
  35. 77
      src/Avalonia.Styling/StyledElement.cs
  36. 17
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs
  37. 16
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs
  38. 23
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs
  39. 22
      tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs
  40. 2
      tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs
  41. 46
      tests/Avalonia.Benchmarks/Layout/CalendarBenchmark.cs
  42. 36
      tests/Avalonia.Benchmarks/NullFormattedTextImpl.cs
  43. 78
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  44. 28
      tests/Avalonia.Benchmarks/NullThreadingPlatform.cs
  45. 46
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  46. 31
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  47. 32
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  48. 28
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  49. 24
      tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
  50. 33
      tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs

4
readme.md

@ -10,7 +10,7 @@
**Avalonia** is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS and with experimental support for Android and iOS.
**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development. To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239).
**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and [breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) as we continue along into this project's development. To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239).
| Control catalog | Desktop platforms | Mobile platforms |
|---|---|---|
@ -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).
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/)
Use these commands in the Package Manager console to install Avalonia manually:

1
src/Avalonia.Base/AvaloniaObject.cs

@ -34,7 +34,6 @@ namespace Avalonia
public AvaloniaObject()
{
VerifyAccess();
AvaloniaPropertyRegistry.Instance.NotifyInitialized(this);
}
/// <summary>

39
src/Avalonia.Base/AvaloniaProperty.cs

@ -20,7 +20,6 @@ namespace Avalonia
public static readonly object UnsetValue = new UnsetValueType();
private static int s_nextId;
private readonly Subject<AvaloniaPropertyChangedEventArgs> _initialized;
private readonly Subject<AvaloniaPropertyChangedEventArgs> _changed;
private readonly PropertyMetadata _defaultMetadata;
private readonly Dictionary<Type, PropertyMetadata> _metadata;
@ -53,7 +52,6 @@ namespace Avalonia
throw new ArgumentException("'name' may not contain periods.");
}
_initialized = new Subject<AvaloniaPropertyChangedEventArgs>();
_changed = new Subject<AvaloniaPropertyChangedEventArgs>();
_metadata = new Dictionary<Type, PropertyMetadata>();
@ -81,7 +79,6 @@ namespace Avalonia
Contract.Requires<ArgumentNullException>(source != null);
Contract.Requires<ArgumentNullException>(ownerType != null);
_initialized = source._initialized;
_changed = source._changed;
_metadata = new Dictionary<Type, PropertyMetadata>();
@ -136,22 +133,6 @@ namespace Avalonia
/// </summary>
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>
/// Gets an observable that is fired when this property changes on any
/// <see cref="AvaloniaObject"/> instance.
@ -488,26 +469,6 @@ namespace Avalonia
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>
/// Notifies the <see cref="Changed"/> observable.
/// </summary>

45
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -415,51 +415,6 @@ namespace Avalonia
_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
{
public AvaloniaProperty Property { get; }

15
src/Avalonia.Base/DirectPropertyBase.cs

@ -101,21 +101,6 @@ namespace Avalonia
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/>
internal override void RouteClearValue(IAvaloniaObject o)
{

15
src/Avalonia.Base/StyledPropertyBase.cs

@ -181,21 +181,6 @@ namespace Avalonia
/// <inheritdoc/>
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/>
internal override void RouteClearValue(IAvaloniaObject o)
{

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

@ -183,11 +183,16 @@ namespace Avalonia.Utilities
for (int c = 0; c < _count; c++)
{
var r = _data[c];
TSubscriber target = null;
r.Subscriber?.TryGetTarget(out target);
//Mark current index as first empty
if (r.Subscriber == null && empty == -1)
if (target == null && empty == -1)
empty = c;
//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[empty] = r;

8
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -373,7 +373,11 @@ namespace Avalonia.Controls
public bool 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 =
@ -656,8 +660,6 @@ namespace Avalonia.Controls
HorizontalScrollBarVisibilityProperty,
VerticalScrollBarVisibilityProperty);
PseudoClass<DataGrid, bool>(IsValidProperty, x => !x, ":invalid");
ItemsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnItemsPropertyChanged(e));
CanUserResizeColumnsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnCanUserResizeColumnsChanged(e));
ColumnWidthProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnColumnWidthChanged(e));

4
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@ -326,7 +326,7 @@ namespace Avalonia.Controls
if (OwningGrid != null && OwningGrid.ColumnHeaders != null)
{
args.Device.Capture(this);
args.Pointer.Capture(this);
_dragMode = DragMode.MouseDown;
_frozenColumnsWidth = OwningGrid.ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth();
@ -391,7 +391,7 @@ namespace Avalonia.Controls
SetDragCursor(mousePosition);
// Variables that track drag mode states get reset in DataGridColumnHeader_LostMouseCapture
args.Device.Capture(null);
args.Pointer.Capture(null);
OnLostMouseCapture();
_dragMode = DragMode.None;
handled = true;

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

@ -45,7 +45,7 @@
<Path Name="SortIcon"
Grid.Column="1"
Fill="#FF444444"
Fill="{TemplateBinding Foreground}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Stretch="Uniform"
@ -113,7 +113,7 @@
<Style Selector="DataGridRow /template/ Rectangle#BackgroundRectangle">
<Setter Property="IsVisible" Value="False"/>
<Setter Property="Fill" Value="#FFBADDE9" />
<Setter Property="Fill" Value="{DynamicResource HighlightBrush}" />
</Style>
<Style Selector="DataGridRow:pointerover /template/ Rectangle#BackgroundRectangle">
@ -126,6 +126,10 @@
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="DataGridRow:selected">
<Setter Property="Foreground" Value="{DynamicResource HighlightForegroundBrush}" />
</Style>
<Style Selector="DataGridRowHeader">
<Setter Property="Template">
<ControlTemplate>
@ -139,7 +143,7 @@
</Style>
<Style Selector="DataGridRowGroupHeader">
<Setter Property="Background" Value="#FFE4E8EA" />
<Setter Property="Background" Value="{DynamicResource ThemeControlMidHighBrush}" />
<Setter Property="Height" Value="20"/>
<Setter Property="Template">
<ControlTemplate>
@ -148,7 +152,6 @@
ColumnDefinitions="Auto,Auto,Auto,Auto"
RowDefinitions="Auto,*,Auto">
<Rectangle Grid.Column="1" Grid.ColumnSpan="5" Fill="#FFFFFFFF" Height="1"/>
<Rectangle Grid.Column="1" Grid.Row="1" Name="IndentSpacer" />
<ToggleButton Grid.Column="2" Grid.Row="1" Name="ExpanderButton" Margin="2,0,0,0"/>
@ -169,7 +172,7 @@
<Setter Property="Template">
<ControlTemplate>
<Border Grid.Column="0" Width="20" Height="20" Background="Transparent" HorizontalAlignment="Center" VerticalAlignment="Center">
<Path Fill="Black"
<Path Fill="{TemplateBinding Foreground}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="M 0 2 L 4 6 L 0 10 Z" />
@ -204,10 +207,10 @@
</Setter>
<Setter Property="Template">
<ControlTemplate>
<Border BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}">
<Grid
RowDefinitions="Auto,*,Auto,Auto"
ColumnDefinitions="Auto,*,Auto">
<Border Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}">
<Grid RowDefinitions="Auto,*,Auto,Auto" ColumnDefinitions="Auto,*,Auto">
<DataGridColumnHeader Name="PART_TopLeftCornerHeader" Width="22" />
<DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter" Grid.Column="1"/>

2
src/Avalonia.Controls/AutoCompleteBox.cs

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

25
src/Avalonia.Controls/Button.cs

@ -91,7 +91,11 @@ namespace Avalonia.Controls
CommandProperty.Changed.Subscribe(CommandChanged);
IsDefaultProperty.Changed.Subscribe(IsDefaultChanged);
IsCancelProperty.Changed.Subscribe(IsCancelChanged);
PseudoClass<Button>(IsPressedProperty, ":pressed");
}
public Button()
{
UpdatePseudoClasses(IsPressed);
}
/// <summary>
@ -312,6 +316,20 @@ namespace Avalonia.Controls
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)
{
base.UpdateDataValidation(property, value);
@ -474,5 +492,10 @@ namespace Avalonia.Controls
OnClick();
}
}
private void UpdatePseudoClasses(bool isPressed)
{
PseudoClasses.Set(":pressed", isPressed);
}
}
}

28
src/Avalonia.Controls/ButtonSpinner.cs

@ -1,5 +1,6 @@
using System;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
@ -34,6 +35,11 @@ namespace Avalonia.Controls
public static readonly StyledProperty<Location> ButtonSpinnerLocationProperty =
AvaloniaProperty.Register<ButtonSpinner, Location>(nameof(ButtonSpinnerLocation), Location.Right);
public ButtonSpinner()
{
UpdatePseudoClasses(ButtonSpinnerLocation);
}
private Button _decreaseButton;
/// <summary>
/// Gets or sets the DecreaseButton template part.
@ -85,8 +91,6 @@ namespace Avalonia.Controls
static ButtonSpinner()
{
AllowSpinProperty.Changed.Subscribe(AllowSpinChanged);
PseudoClass<ButtonSpinner, Location>(ButtonSpinnerLocationProperty, location => location == Location.Left, ":left");
PseudoClass<ButtonSpinner, Location>(ButtonSpinnerLocationProperty, location => location == Location.Right, ":right");
}
/// <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)
{
SetButtonUsage();
@ -259,5 +277,11 @@ namespace Avalonia.Controls
OnSpin(new SpinEventArgs(SpinEvent, direction));
}
}
private void UpdatePseudoClasses(Location location)
{
PseudoClasses.Set(":left", location == Location.Left);
PseudoClasses.Set(":right", location == Location.Right);
}
}
}

12
src/Avalonia.Controls/Calendar/CalendarItem.cs

@ -943,8 +943,8 @@ namespace Avalonia.Controls.Primitives
{
CalendarDayButton b = (CalendarDayButton)sender;
// The button is in Pressed state. Change the state to normal.
if (e.Device.Captured == b)
e.Device.Capture(null);
if (e.Pointer.Captured == b)
e.Pointer.Capture(null);
_lastCalendarDayButton = b;
}
}
@ -1213,8 +1213,8 @@ namespace Avalonia.Controls.Primitives
{
CalendarButton b = (CalendarButton)sender;
// The button is in Pressed state. Change the state to normal.
if (e.Device.Captured == b)
e.Device.Capture(null);
if (e.Pointer.Captured == b)
e.Pointer.Capture(null);
//b.ReleaseMouseCapture();
_lastCalendarButton = b;
@ -1224,7 +1224,7 @@ namespace Avalonia.Controls.Primitives
{
if (_lastCalendarDayButton != null)
{
e.Device.Capture(_lastCalendarDayButton);
e.Pointer.Capture(_lastCalendarDayButton);
}
}
@ -1232,7 +1232,7 @@ namespace Avalonia.Controls.Primitives
{
if (_lastCalendarButton != null)
{
e.Device.Capture(_lastCalendarButton);
e.Pointer.Capture(_lastCalendarButton);
}
}

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

@ -788,7 +788,7 @@ namespace Avalonia.Controls
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)

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)
{
_owner.OnSelectedDatesCollectionChanged(new SelectionChangedEventArgs(null, addedItems, removedItems));
_owner.OnSelectedDatesCollectionChanged(new SelectionChangedEventArgs(null, removedItems, addedItems));
}
/// <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.UpdateMonths();
_isRangeAdded = false;

20
src/Avalonia.Controls/ControlExtensions.cs

@ -69,26 +69,6 @@ namespace Avalonia.Controls
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>
/// Sets a pseudoclass depending on an observable trigger.
/// </summary>

41
src/Avalonia.Controls/Expander.cs

@ -1,5 +1,6 @@
using Avalonia.Animation;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
namespace Avalonia.Controls
{
@ -30,16 +31,14 @@ namespace Avalonia.Controls
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));
}
public Expander()
{
UpdatePseudoClasses(ExpandDirection);
}
public IPageTransition ContentTransition
{
get => GetValue(ContentTransitionProperty);
@ -55,7 +54,11 @@ namespace Avalonia.Controls
public bool 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)
@ -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);
}
}
}

39
src/Avalonia.Controls/ItemsControl.cs

@ -236,25 +236,7 @@ namespace Avalonia.Controls
// it was added to the Items collection.
if (container.ContainerControl != null && container.ContainerControl != container.Item)
{
if (ItemContainerGenerator.ContainerType == null)
{
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);
}
LogicalChildren.Add(container.ContainerControl);
}
}
}
@ -272,24 +254,7 @@ namespace Avalonia.Controls
// when it is removed from the Items collection.
if (container?.ContainerControl != container?.Item)
{
if (ItemContainerGenerator.ContainerType == null)
{
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);
}
LogicalChildren.Remove(container.ContainerControl);
}
}
}

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

@ -8,6 +8,7 @@ using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Primitives;
using Avalonia.Rendering;
using Avalonia.Data;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Notifications
@ -68,15 +69,12 @@ namespace Avalonia.Controls.Notifications
Install(host);
});
}
UpdatePseudoClasses(Position);
}
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);
VerticalAlignmentProperty.OverrideDefaultValue<WindowNotificationManager>(Layout.VerticalAlignment.Stretch);
}
@ -143,6 +141,20 @@ namespace Avalonia.Controls.Notifications
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>
/// Installs the <see cref="WindowNotificationManager"/> within the <see cref="AdornerLayer"/>
/// of the host <see cref="Window"/>.
@ -155,6 +167,14 @@ namespace Avalonia.Controls.Notifications
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);
}
}

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.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Controls.Presenters;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
@ -15,6 +13,8 @@ using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Controls.Primitives
{
/// <summary>
@ -25,8 +25,8 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Defines the <see cref="Child"/> property.
/// </summary>
public static readonly StyledProperty<Control> ChildProperty =
AvaloniaProperty.Register<Popup, Control>(nameof(Child));
public static readonly StyledProperty<Control?> ChildProperty =
AvaloniaProperty.Register<Popup, Control?>(nameof(Child));
/// <summary>
/// Defines the <see cref="IsOpen"/> property.
@ -43,11 +43,13 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
AvaloniaProperty.Register<Popup, PlacementMode>(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
#pragma warning disable 618
/// <summary>
/// Defines the <see cref="ObeyScreenEdges"/> property.
/// </summary>
public static readonly StyledProperty<bool> ObeyScreenEdgesProperty =
AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges), true);
#pragma warning restore 618
/// <summary>
/// Defines the <see cref="HorizontalOffset"/> property.
@ -64,8 +66,8 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Defines the <see cref="PlacementTarget"/> property.
/// </summary>
public static readonly StyledProperty<Control> PlacementTargetProperty =
AvaloniaProperty.Register<Popup, Control>(nameof(PlacementTarget));
public static readonly StyledProperty<Control?> PlacementTargetProperty =
AvaloniaProperty.Register<Popup, Control?>(nameof(PlacementTarget));
/// <summary>
/// Defines the <see cref="StaysOpen"/> property.
@ -80,12 +82,8 @@ namespace Avalonia.Controls.Primitives
AvaloniaProperty.Register<Popup, bool>(nameof(Topmost));
private bool _isOpen;
private IPopupHost _popupHost;
private TopLevel _topLevel;
private IDisposable _nonClientListener;
private IDisposable _presenterSubscription;
bool _ignoreIsOpenChanged = false;
private List<IDisposable> _bindings = new List<IDisposable>();
private bool _ignoreIsOpenChanged;
private PopupOpenState? _openState;
/// <summary>
/// Initializes static members of the <see cref="Popup"/> class.
@ -94,31 +92,26 @@ namespace Avalonia.Controls.Primitives
{
IsHitTestVisibleProperty.OverrideDefaultValue<Popup>(false);
ChildProperty.Changed.AddClassHandler<Popup>((x, e) => x.ChildChanged(e));
IsOpenProperty.Changed.AddClassHandler<Popup>((x, e) => x.IsOpenChanged(e));
}
public Popup()
{
IsOpenProperty.Changed.AddClassHandler<Popup>((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs<bool>)e));
}
/// <summary>
/// Raised when the popup closes.
/// </summary>
public event EventHandler Closed;
public event EventHandler? Closed;
/// <summary>
/// Raised when the popup opens.
/// </summary>
public event EventHandler Opened;
public event EventHandler? Opened;
public IPopupHost Host => _popupHost;
public IPopupHost? Host => _openState?.PopupHost;
/// <summary>
/// Gets or sets the control to display in the popup.
/// </summary>
[Content]
public Control Child
public Control? Child
{
get { return GetValue(ChildProperty); }
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
/// a specialized dependency resolver into the <see cref="PopupRoot"/>'s constructor.
/// </remarks>
public IAvaloniaDependencyResolver DependencyResolver
public IAvaloniaDependencyResolver? DependencyResolver
{
get;
set;
@ -183,7 +176,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Gets or sets the control that is used to determine the popup's position.
/// </summary>
public Control PlacementTarget
public Control? PlacementTarget
{
get { return GetValue(PlacementTargetProperty); }
set { SetValue(PlacementTargetProperty, value); }
@ -211,7 +204,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Gets the root of the popup window.
/// </summary>
IVisual IVisualTreeHost.Root => _popupHost?.HostedVisualTreeRoot;
IVisual? IVisualTreeHost.Root => _openState?.PopupHost.HostedVisualTreeRoot;
/// <summary>
/// Opens the popup.
@ -219,50 +212,91 @@ namespace Avalonia.Controls.Primitives
public void Open()
{
// Popup is currently open
if (_topLevel != null)
if (_openState != null)
{
return;
CloseCurrent();
}
var placementTarget = PlacementTarget ?? this.GetLogicalAncestors().OfType<IVisual>().FirstOrDefault();
if (placementTarget == 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(
"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));
_popupHost.SetChild(Child);
((ISetLogicalParent)_popupHost).SetParent(this);
_popupHost.ConfigurePosition(placementTarget,
PlacementMode, new Point(HorizontalOffset, VerticalOffset));
_popupHost.TemplateApplied += RootTemplateApplied;
var window = _topLevel as Window;
if (window != null)
popupHost.SetChild(Child);
((ISetLogicalParent)popupHost).SetParent(this);
popupHost.ConfigurePosition(
placementTarget,
PlacementMode,
new Point(HorizontalOffset, VerticalOffset));
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
{
var parentPopuproot = _topLevel as PopupRoot;
if (parentPopuproot?.Parent is Popup popup)
var parentPopupRoot = topLevel as PopupRoot;
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())
{
@ -277,14 +311,19 @@ namespace Avalonia.Controls.Primitives
/// </summary>
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())
{
IsOpen = false;
@ -293,41 +332,6 @@ namespace Avalonia.Controls.Primitives
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>
/// Measures the control.
/// </summary>
@ -345,16 +349,22 @@ namespace Avalonia.Controls.Primitives
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>
/// Called when the <see cref="IsOpen"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private void IsOpenChanged(AvaloniaPropertyChangedEventArgs e)
private void IsOpenChanged(AvaloniaPropertyChangedEventArgs<bool> e)
{
if (!_ignoreIsOpenChanged)
{
if ((bool)e.NewValue)
if (e.NewValue.Value)
{
Open();
}
@ -373,7 +383,7 @@ namespace Avalonia.Controls.Primitives
{
LogicalChildren.Clear();
((ISetLogicalParent)e.OldValue)?.SetParent(null);
((ISetLogicalParent?)e.OldValue)?.SetParent(null);
if (e.NewValue != null)
{
@ -394,34 +404,37 @@ namespace Avalonia.Controls.Primitives
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)
{
_popupHost.TemplateApplied -= RootTemplateApplied;
if (_presenterSubscription != null)
if (_openState is null)
{
_presenterSubscription.Dispose();
_presenterSubscription = null;
return;
}
var popupHost = _openState.PopupHost;
popupHost.TemplateApplied -= RootTemplateApplied;
_openState.SetPresenterSubscription(null);
// If the Popup appears in a control template, then the child controls
// that appear in the popup host need to have their TemplatedParent
// properties set.
if (TemplatedParent != null)
if (TemplatedParent != null && popupHost.Presenter != null)
{
_popupHost.Presenter?.ApplyTemplate();
_popupHost.Presenter?.GetObservable(ContentPresenter.ChildProperty)
popupHost.Presenter.ApplyTemplate();
var presenterSubscription = popupHost.Presenter.GetObservable(ContentPresenter.ChildProperty)
.Subscribe(SetTemplatedParentAndApplyChildTemplates);
_openState.SetPresenterSubscription(presenterSubscription);
}
}
@ -440,7 +453,7 @@ namespace Avalonia.Controls.Primitives
if (!(control is IPresenter) && control.TemplatedParent == templatedParent)
{
foreach (IControl child in control.GetVisualChildren())
foreach (IControl child in control.VisualChildren)
{
SetTemplatedParentAndApplyChildTemplates(child);
}
@ -450,22 +463,41 @@ namespace Avalonia.Controls.Primitives
private bool IsChildOrThis(IVisual child)
{
IVisual root = child.GetVisualRoot();
while (root is IHostedVisualTreeRoot hostedRoot )
if (_openState is null)
{
if (root == this._popupHost)
return false;
}
var popupHost = _openState.PopupHost;
IVisual? root = child.VisualRoot;
while (root is IHostedVisualTreeRoot hostedRoot)
{
if (root == popupHost)
{
return true;
root = hostedRoot.Host?.GetVisualRoot();
}
root = hostedRoot.Host?.VisualRoot;
}
return false;
}
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)
{
@ -503,5 +535,36 @@ namespace Avalonia.Controls.Primitives
_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>
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.DragCompletedEvent.AddClassHandler<ScrollBar>((x, e) => x.OnThumbDragComplete(e), RoutingStrategies.Bubble);
}
@ -74,6 +71,7 @@ namespace Avalonia.Controls.Primitives
this.GetObservable(VisibilityProperty).Select(_ => Unit.Default))
.Select(_ => CalculateIsVisible());
this.Bind(IsVisibleProperty, isVisible, BindingPriority.Style);
UpdatePseudoClasses(Orientation);
}
/// <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)
{
base.OnTemplateApplied(e);
@ -252,5 +264,11 @@ namespace Avalonia.Controls.Primitives
{
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(
SelectionChangedEvent,
added ?? Empty,
removed ?? Empty);
removed ?? Empty,
added ?? Empty);
RaiseEvent(changed);
}
}
@ -1055,8 +1055,8 @@ namespace Avalonia.Controls.Primitives
var e = new SelectionChangedEventArgs(
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);
}

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.
using System;
using Avalonia.Interactivity;
using Avalonia.Data;
using Avalonia.Interactivity;
namespace Avalonia.Controls.Primitives
{
@ -51,13 +51,14 @@ namespace Avalonia.Controls.Primitives
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));
}
public ToggleButton()
{
UpdatePseudoClasses(IsChecked);
}
/// <summary>
/// Raised when a <see cref="ToggleButton"/> is checked.
/// </summary>
@ -91,7 +92,11 @@ namespace Avalonia.Controls.Primitives
public bool? IsChecked
{
get => _isChecked;
set => SetAndRaise(IsCheckedProperty, ref _isChecked, value);
set
{
SetAndRaise(IsCheckedProperty, ref _isChecked, value);
UpdatePseudoClasses(value);
}
}
/// <summary>
@ -182,5 +187,12 @@ namespace Avalonia.Controls.Primitives
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.
using System;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Metadata;
@ -46,14 +47,17 @@ namespace Avalonia.Controls.Primitives
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));
IncreaseButtonProperty.Changed.AddClassHandler<Track>((x, e) => x.ButtonChanged(e));
DecreaseButtonProperty.Changed.AddClassHandler<Track>((x, e) => x.ButtonChanged(e));
AffectsArrange<Track>(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty);
}
public Track()
{
UpdatePseudoClasses(Orientation);
}
public double Minimum
{
get { return _minimum; }
@ -276,6 +280,20 @@ namespace Avalonia.Controls.Primitives
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)
{
if (componentLength < 0)
@ -433,5 +451,11 @@ namespace Avalonia.Controls.Primitives
DecreaseButton.IsVisible = visible;
}
}
private void UpdatePseudoClasses(Orientation o)
{
PseudoClasses.Set(":vertical", o == Orientation.Vertical);
PseudoClasses.Set(":horizontal", o == Orientation.Horizontal);
}
}
}

44
src/Avalonia.Controls/ProgressBar.cs

@ -4,6 +4,7 @@
using System;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Layout;
namespace Avalonia.Controls
@ -35,14 +36,15 @@ namespace Avalonia.Controls
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));
IsIndeterminateProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));
}
public ProgressBar()
{
UpdatePseudoClasses(IsIndeterminate, Orientation);
}
public bool IsIndeterminate
{
get => GetValue(IsIndeterminateProperty);
@ -75,6 +77,24 @@ namespace Avalonia.Controls
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/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
@ -121,5 +141,21 @@ namespace Avalonia.Controls
{
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>
public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs
{
internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int newIndex, int oldIndex)
internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int oldIndex, int newIndex)
{
Element = element;
NewIndex = newIndex;
OldIndex = oldIndex;
NewIndex = newIndex;
}
/// <summary>

16
src/Avalonia.Controls/ScrollViewer.cs

@ -249,6 +249,22 @@ namespace Avalonia.Controls
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>
/// Gets a value indicating whether the viewer can scroll horizontally.
/// </summary>

6
src/Avalonia.Controls/SelectionChangedEventArgs.cs

@ -16,13 +16,13 @@ namespace Avalonia.Controls
/// Initializes a new instance of the <see cref="SelectionChangedEventArgs"/> class.
/// </summary>
/// <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>
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)
{
AddedItems = addedItems;
RemovedItems = removedItems;
AddedItems = addedItems;
}
/// <summary>

24
src/Avalonia.Controls/Slider.cs

@ -3,6 +3,7 @@
using System;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
@ -43,8 +44,6 @@ namespace Avalonia.Controls
static Slider()
{
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.DragDeltaEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble);
Thumb.DragCompletedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragCompleted(e), RoutingStrategies.Bubble);
@ -55,6 +54,7 @@ namespace Avalonia.Controls
/// </summary>
public Slider()
{
UpdatePseudoClasses(Orientation);
}
/// <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>
/// Called when user start dragging the <see cref="Thumb"/>.
/// </summary>
@ -190,5 +204,11 @@ namespace Avalonia.Controls
return value;
}
private void UpdatePseudoClasses(Orientation o)
{
PseudoClasses.Set(":vertical", o == Orientation.Vertical);
PseudoClasses.Set(":horizontal", o == Orientation.Horizontal);
}
}
}

4
src/Avalonia.Controls/TreeView.cs

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

42
src/Avalonia.Input/InputElement.cs

@ -4,6 +4,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
@ -181,10 +183,11 @@ namespace Avalonia.Input
PointerReleasedEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerReleased(e));
PointerCaptureLostEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerCaptureLost(e));
PointerWheelChangedEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerWheelChanged(e));
}
PseudoClass<InputElement, bool>(IsEffectivelyEnabledProperty, x => !x, ":disabled");
PseudoClass<InputElement>(IsFocusedProperty, ":focus");
PseudoClass<InputElement>(IsPointerOverProperty, ":pointerover");
public InputElement()
{
UpdatePseudoClasses(IsFocused, IsPointerOver);
}
/// <summary>
@ -372,7 +375,11 @@ namespace Avalonia.Input
public bool 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>();
@ -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>
/// Updates the <see cref="IsEffectivelyEnabled"/> property value according to the parent
/// control's enabled state and <see cref="IsEnabledCore"/>.
@ -578,5 +599,18 @@ namespace Avalonia.Input
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/>
public void ExecuteInitialLayoutPass(ILayoutRoot root)
{
Measure(root);
Arrange(root);
try
{
_running = true;
Measure(root);
Arrange(root);
}
finally
{
_running = false;
}
// 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

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

77
src/Avalonia.Styling/StyledElement.cs

@ -488,83 +488,6 @@ namespace Avalonia
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)
{
switch (e.Action)

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

@ -16,19 +16,6 @@ namespace Avalonia.Base.UnitTests
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
{
}
@ -46,9 +33,5 @@ namespace Avalonia.Base.UnitTests
public static readonly AttachedProperty<string> FooProperty =
Class1.FooProperty.AddOwner<Class2>();
}
private class Class3 : Base
{
}
}
}

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

@ -424,22 +424,6 @@ namespace Avalonia.Base.UnitTests
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]
public void Binding_Error_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);
}
[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]
public void Changed_Observable_Fired()
{
@ -141,11 +123,6 @@ namespace Avalonia.Base.UnitTests
OverrideMetadata(typeof(T), metadata);
}
internal override void NotifyInitialized(IAvaloniaObject o)
{
throw new NotImplementedException();
}
internal override IDisposable RouteBind(
IAvaloniaObject o,
IObservable<BindingValue<object>> source,

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

@ -1,33 +1,12 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Subjects;
using Avalonia.Data;
using Xunit;
namespace Avalonia.Base.UnitTests
{
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]
public void IsDirect_Property_Returns_True()
{
@ -69,7 +48,6 @@ namespace Avalonia.Base.UnitTests
var p2 = p1.AddOwner<Class2>(o => null, (o, v) => { });
Assert.Same(p1.Changed, p2.Changed);
Assert.Same(p1.Initialized, p2.Initialized);
}
private class Class1 : AvaloniaObject

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

@ -6,7 +6,7 @@ namespace Avalonia.Benchmarks.Base
[MemoryDiagnoser]
public class AvaloniaObjectInitializationBenchmark
{
[Benchmark(OperationsPerInvoke = 1000)]
[Benchmark]
public Button InitializeButton()
{
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;
}
}

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

@ -51,10 +51,9 @@ namespace Avalonia.Controls.UnitTests
Assert.Single(target.GetLogicalChildren());
var child = target.GetLogicalChildren().Single();
var child = GetContainerTextBlock(target.GetLogicalChildren().Single());
Assert.IsType<TextBlock>(child);
Assert.Equal("Foo", ((TextBlock)child).Text);
Assert.Equal("Foo", child.Text);
}
[Fact]
@ -98,20 +97,18 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(3, target.GetLogicalChildren().Count());
var child = target.GetLogicalChildren().First();
var child = GetContainerTextBlock(target.GetLogicalChildren().First());
Assert.IsType<TextBlock>(child);
Assert.Equal("Foo", ((TextBlock)child).Text);
Assert.Equal("Foo", child.Text);
var newItems = items.ToList();
newItems.RemoveAt(0);
target.Items = newItems;
child = target.GetLogicalChildren().First();
child = GetContainerTextBlock(target.GetLogicalChildren().First());
Assert.IsType<TextBlock>(child);
Assert.Equal("Bar", ((TextBlock)child).Text);
Assert.Equal("Bar", child.Text);
}
[Fact]
@ -136,20 +133,18 @@ namespace Avalonia.Controls.UnitTests
Assert.Single(target.GetLogicalChildren());
var child = target.GetLogicalChildren().Single();
var child = GetContainerTextBlock(target.GetLogicalChildren().Single());
Assert.IsType<TextBlock>(child);
Assert.Equal("Foo", ((TextBlock)child).Text);
Assert.Equal("Foo", child.Text);
var newItems = items.ToList();
newItems.RemoveAt(0);
target.Items = newItems;
child = target.GetLogicalChildren().Single();
child = GetContainerTextBlock(target.GetLogicalChildren().Single());
Assert.IsType<TextBlock>(child);
Assert.Equal("Bar", ((TextBlock)child).Text);
Assert.Equal("Bar", child.Text);
}
[Fact]
@ -197,10 +192,9 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(3, target.GetLogicalChildren().Count());
var child = target.GetLogicalChildren().First();
var child = GetContainerTextBlock(target.GetLogicalChildren().First());
Assert.IsType<TextBlock>(child);
Assert.Equal("Foo", ((TextBlock)child).Text);
Assert.Equal("Foo", child.Text);
target.Items = null;
@ -233,7 +227,7 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal("FooBar", target.SelectedItem);
var child = target.GetVisualDescendants().LastOrDefault();
var child = GetContainerTextBlock(target.GetVisualDescendants().LastOrDefault());
Assert.IsType<TextBlock>(child);
Assert.Equal("FooBar", ((TextBlock)child).Text);
@ -261,14 +255,13 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(3, target.GetLogicalChildren().Count());
var child = target.GetLogicalChildren().First();
var child = GetContainerTextBlock(target.GetLogicalChildren().First());
Assert.IsType<TextBlock>(child);
Assert.Equal("Foo", ((TextBlock)child).Text);
Assert.Equal("Foo", child.Text);
items.RemoveAt(0);
child = target.GetLogicalChildren().First();
child = GetContainerTextBlock(target.GetLogicalChildren().First());
Assert.IsType<TextBlock>(child);
Assert.Equal("Bar", ((TextBlock)child).Text);
@ -314,5 +307,12 @@ namespace Avalonia.Controls.UnitTests
[~CarouselPresenter.PageTransitionProperty] = control[~Carousel.PageTransitionProperty],
}.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]
public void Container_Child_Should_Have_LogicalParent_Set_To_Container()
public void Container_Should_Have_LogicalParent_Set_To_ItemsControl()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
@ -87,7 +87,7 @@ namespace Avalonia.Controls.UnitTests
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]
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 child = new Control();
@ -202,7 +202,7 @@ namespace Avalonia.Controls.UnitTests
var logical = (ILogical)target;
Assert.Equal(1, logical.LogicalChildren.Count);
Assert.IsType<TextBlock>(logical.LogicalChildren[0]);
Assert.IsType<ContentPresenter>(logical.LogicalChildren[0]);
}
[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
{
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]
public void Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent()
{

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

@ -65,6 +65,30 @@ namespace Avalonia.Controls.UnitTests
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)
{
return new Grid

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

@ -374,5 +374,38 @@ namespace Avalonia.Layout.UnitTests
Assert.True(control.Measured);
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);
}
}
}

Loading…
Cancel
Save