Browse Source

Merge branch 'master' into fixes/3550-menu-sizing

pull/3553/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
649c655bbc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      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. 8
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  8. 8
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  9. 25
      src/Avalonia.Controls/Button.cs
  10. 28
      src/Avalonia.Controls/ButtonSpinner.cs
  11. 20
      src/Avalonia.Controls/ControlExtensions.cs
  12. 41
      src/Avalonia.Controls/Expander.cs
  13. 30
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  14. 24
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  15. 24
      src/Avalonia.Controls/Primitives/ToggleButton.cs
  16. 28
      src/Avalonia.Controls/Primitives/Track.cs
  17. 44
      src/Avalonia.Controls/ProgressBar.cs
  18. 24
      src/Avalonia.Controls/Slider.cs
  19. 42
      src/Avalonia.Input/InputElement.cs
  20. 28
      src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs
  21. 77
      src/Avalonia.Styling/StyledElement.cs
  22. 17
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs
  23. 16
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs
  24. 23
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs
  25. 22
      tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs
  26. 2
      tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs
  27. 46
      tests/Avalonia.Benchmarks/Layout/CalendarBenchmark.cs
  28. 36
      tests/Avalonia.Benchmarks/NullFormattedTextImpl.cs
  29. 78
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  30. 28
      tests/Avalonia.Benchmarks/NullThreadingPlatform.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).
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)
{

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

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

@ -207,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"/>

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

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

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

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

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

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

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

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;
}
}
Loading…
Cancel
Save