From 4a752c3f48f355a442694574fa1e068aa0cfdf10 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Apr 2020 09:39:22 +0200 Subject: [PATCH 01/44] Get non-animated change information. - Pass information for all property changes to `OnPropertyChangedCore`; whether they result in an effective value change or not - Added `GetBaseValue` to get a value with a specified priority - Change the signature of `OnPropertyChanged` again to take an `AvaloniaPropertyChangedEventArgs` --- src/Avalonia.Animation/Animatable.cs | 18 +- src/Avalonia.Base/AvaloniaObject.cs | 198 +++++++++++------- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 59 ++++++ src/Avalonia.Base/AvaloniaProperty.cs | 7 + .../AvaloniaPropertyChangedEventArgs.cs | 23 +- .../AvaloniaPropertyChangedEventArgs`1.cs | 24 ++- src/Avalonia.Base/DirectPropertyBase.cs | 5 + src/Avalonia.Base/IAvaloniaObject.cs | 13 ++ .../PropertyStore/BindingEntry.cs | 20 +- .../PropertyStore/ConstantValueEntry.cs | 14 +- src/Avalonia.Base/PropertyStore/IValue.cs | 6 +- src/Avalonia.Base/PropertyStore/IValueSink.cs | 6 +- .../PropertyStore/LocalValueEntry.cs | 11 +- .../PropertyStore/PriorityValue.cs | 161 +++++++++----- src/Avalonia.Base/StyledPropertyBase.cs | 7 + src/Avalonia.Base/ValueStore.cs | 69 +++--- src/Avalonia.Controls.DataGrid/DataGrid.cs | 2 +- src/Avalonia.Controls/Button.cs | 12 +- src/Avalonia.Controls/ButtonSpinner.cs | 12 +- src/Avalonia.Controls/Calendar/DatePicker.cs | 12 +- src/Avalonia.Controls/Expander.cs | 12 +- .../WindowNotificationManager.cs | 12 +- src/Avalonia.Controls/Primitives/ScrollBar.cs | 20 +- src/Avalonia.Controls/Primitives/Track.cs | 12 +- src/Avalonia.Controls/ProgressBar.cs | 16 +- .../Repeater/ItemsRepeater.cs | 28 +-- src/Avalonia.Controls/Slider.cs | 12 +- src/Avalonia.Controls/Window.cs | 12 +- src/Avalonia.Input/InputElement.cs | 12 +- src/Avalonia.Layout/StackLayout.cs | 6 +- src/Avalonia.Layout/UniformGridLayout.cs | 35 ++-- .../Styling/PropertySetterBindingInstance.cs | 2 +- src/Avalonia.Visuals/Media/DrawingImage.cs | 10 +- .../AvaloniaObjectTests_GetValue.cs | 51 +++++ .../AvaloniaObjectTests_OnPropertyChanged.cs | 142 +++++++++++++ .../AvaloniaPropertyTests.cs | 19 ++ .../PriorityValueTests.cs | 98 ++++++++- .../Xaml/InitializationOrderTracker.cs | 6 +- .../Rendering/ImmediateRendererTests.cs | 8 +- 39 files changed, 831 insertions(+), 361 deletions(-) create mode 100644 tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index fa1e955153..324ff06452 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -62,30 +62,26 @@ namespace Avalonia.Animation } } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (_transitions is null || _previousTransitions is null || priority == BindingPriority.Animation) + if (_transitions is null || _previousTransitions is null || change.Priority == BindingPriority.Animation) return; // PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations). foreach (var transition in _transitions) { - if (transition.Property == property) + if (transition.Property == change.Property) { - if (_previousTransitions.TryGetValue(property, out var dispose)) + if (_previousTransitions.TryGetValue(change.Property, out var dispose)) dispose.Dispose(); var instance = transition.Apply( this, Clock ?? Avalonia.Animation.Clock.GlobalClock, - oldValue.GetValueOrDefault(), - newValue.GetValueOrDefault()); + change.OldValue.GetValueOrDefault(), + change.NewValue.GetValueOrDefault()); - _previousTransitions[property] = instance; + _previousTransitions[change.Property] = instance; return; } } diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index ed36e6da43..546ab78e31 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -259,6 +259,21 @@ namespace Avalonia return registered.InvokeGetter(this); } + /// + public Optional GetBaseValue(StyledPropertyBase property, BindingPriority maxPriority) + { + property = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); + + if (_values is object && + _values.TryGetValue(property, maxPriority, out var value)) + { + return value; + } + + return default; + } + /// /// Checks whether a is animating. /// @@ -458,29 +473,43 @@ namespace Avalonia return _propertyChanged?.GetInvocationList(); } - void IValueSink.ValueChanged( - StyledPropertyBase property, - BindingPriority priority, - Optional oldValue, - BindingValue newValue) + void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change) { - oldValue = oldValue.HasValue ? oldValue : GetInheritedOrDefault(property); - newValue = newValue.HasValue ? newValue : newValue.WithValue(GetInheritedOrDefault(property)); + var property = (StyledPropertyBase)change.Property; - LogIfError(property, newValue); + LogIfError(property, change.NewValue); - if (!EqualityComparer.Default.Equals(oldValue.Value, newValue.Value)) + // If the change is to the effective value of the property and no old/new value is set + // then fill in the old/new value from property inheritance/default value. We don't do + // this for non-effective value changes because these are only needed for property + // transitions, where knowing e.g. that an inherited value is active at an arbitrary + // priority isn't of any use and would introduce overhead. + if (change.IsEffectiveValueChange && !change.OldValue.HasValue) { - RaisePropertyChanged(property, oldValue, newValue, priority); + change.SetOldValue(GetInheritedOrDefault(property)); + } - Logger.TryGet(LogEventLevel.Verbose)?.Log( - LogArea.Property, - this, - "{Property} changed from {$Old} to {$Value} with priority {Priority}", - property, - oldValue, - newValue, - (BindingPriority)priority); + if (change.IsEffectiveValueChange && !change.NewValue.HasValue) + { + change.SetNewValue(GetInheritedOrDefault(property)); + } + + if (!change.IsEffectiveValueChange || + !EqualityComparer.Default.Equals(change.OldValue.Value, change.NewValue.Value)) + { + RaisePropertyChanged(change); + + if (change.IsEffectiveValueChange) + { + Logger.TryGet(LogEventLevel.Verbose)?.Log( + LogArea.Property, + this, + "{Property} changed from {$Old} to {$Value} with priority {Priority}", + property, + change.OldValue, + change.NewValue, + change.Priority); + } } } @@ -489,7 +518,13 @@ namespace Avalonia IPriorityValueEntry entry, Optional oldValue) { - ((IValueSink)this).ValueChanged(property, BindingPriority.Unset, oldValue, default); + var change = new AvaloniaPropertyChangedEventArgs( + this, + property, + oldValue, + default, + BindingPriority.Unset); + ((IValueSink)this).ValueChanged(change); } /// @@ -575,15 +610,20 @@ namespace Avalonia /// /// Called when a avalonia property changes on the object. /// - /// The property whose value has changed. - /// The old value of the property. - /// The new value of the property. - /// The priority of the new value. - protected virtual void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + /// The property change details. + protected virtual void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) + { + if (change.IsEffectiveValueChange) + { + OnPropertyChanged(change); + } + } + + /// + /// Called when a avalonia property changes on the object. + /// + /// The property change details. + protected virtual void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { } @@ -600,57 +640,12 @@ namespace Avalonia BindingValue newValue, BindingPriority priority = BindingPriority.LocalValue) { - property = property ?? throw new ArgumentNullException(nameof(property)); - - VerifyAccess(); - - property.Notifying?.Invoke(this, true); - - try - { - AvaloniaPropertyChangedEventArgs e = null; - var hasChanged = property.HasChangedSubscriptions; - - if (hasChanged || _propertyChanged != null) - { - e = new AvaloniaPropertyChangedEventArgs( - this, - property, - oldValue, - newValue, - priority); - } - - OnPropertyChanged(property, oldValue, newValue, priority); - - if (hasChanged) - { - property.NotifyChanged(e); - } - - _propertyChanged?.Invoke(this, e); - - if (_inpcChanged != null) - { - var inpce = new PropertyChangedEventArgs(property.Name); - _inpcChanged(this, inpce); - } - - if (property.Inherits && _inheritanceChildren != null) - { - foreach (var child in _inheritanceChildren) - { - child.InheritedPropertyChanged( - property, - oldValue, - newValue.ToOptional()); - } - } - } - finally - { - property.Notifying?.Invoke(this, false); - } + RaisePropertyChanged(new AvaloniaPropertyChangedEventArgs( + this, + property, + oldValue, + newValue, + priority)); } /// @@ -689,7 +684,9 @@ namespace Avalonia return property.GetDefaultValue(GetType()); } - private T GetValueOrInheritedOrDefault(StyledPropertyBase property) + private T GetValueOrInheritedOrDefault( + StyledPropertyBase property, + BindingPriority maxPriority = BindingPriority.Animation) { var o = this; var inherits = property.Inherits; @@ -699,7 +696,7 @@ namespace Avalonia { var values = o._values; - if (values?.TryGetValue(property, out value) == true) + if (values?.TryGetValue(property, maxPriority, out value) == true) { return value; } @@ -715,6 +712,45 @@ namespace Avalonia return property.GetDefaultValue(GetType()); } + protected internal void RaisePropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + VerifyAccess(); + + change.Property.Notifying?.Invoke(this, true); + + try + { + OnPropertyChangedCore(change); + + if (change.IsEffectiveValueChange) + { + change.Property.NotifyChanged(change); + _propertyChanged?.Invoke(this, change); + + if (_inpcChanged != null) + { + var inpce = new PropertyChangedEventArgs(change.Property.Name); + _inpcChanged(this, inpce); + } + + if (change.Property.Inherits && _inheritanceChildren != null) + { + foreach (var child in _inheritanceChildren) + { + child.InheritedPropertyChanged( + change.Property, + change.OldValue, + change.NewValue.ToOptional()); + } + } + } + } + finally + { + change.Property.Notifying?.Invoke(this, false); + } + } + /// /// Sets the value of a direct property. /// diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 4fc65a3ed4..173c5c1a94 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -448,6 +448,65 @@ namespace Avalonia }; } + /// + /// Gets an base value. + /// + /// The object. + /// The property. + /// The maximum priority for the value. + /// + /// For styled properties, gets the value of the property if set on the object with a + /// priority equal or lower to , otherwise + /// . Note that this method does not return + /// property values that come from inherited or default values. + /// + /// For direct properties returns . + /// + public static object GetBaseValue( + this IAvaloniaObject target, + AvaloniaProperty property, + BindingPriority maxPriority) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + return property.RouteGetBaseValue(target, maxPriority); + } + + /// + /// Gets an base value. + /// + /// The object. + /// The property. + /// The maximum priority for the value. + /// + /// For styled properties, gets the value of the property if set on the object with a + /// priority equal or lower to , otherwise + /// . Note that this method does not return property values + /// that come from inherited or default values. + /// + /// For direct properties returns + /// . + /// + public static Optional GetBaseValue( + this IAvaloniaObject target, + AvaloniaProperty property, + BindingPriority maxPriority) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + return property switch + { + StyledPropertyBase styled => target.GetBaseValue(styled, maxPriority), + DirectPropertyBase direct => target.GetValue(direct), + _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") + }; + } + /// /// Sets a value. /// diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 74d7039751..daa7191cc5 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -496,6 +496,13 @@ namespace Avalonia /// The object instance. internal abstract object RouteGetValue(IAvaloniaObject o); + /// + /// Routes an untyped GetBaseValue call to a typed call. + /// + /// The object instance. + /// The maximum priority for the value. + internal abstract object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority); + /// /// Routes an untyped SetValue call to a typed call. /// diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs index 0f09747865..c1a2832fde 100644 --- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs @@ -16,6 +16,7 @@ namespace Avalonia { Sender = sender; Priority = priority; + IsEffectiveValueChange = true; } /// @@ -35,19 +36,11 @@ namespace Avalonia /// /// Gets the old value of the property. /// - /// - /// The old value of the property or if the - /// property previously had no value. - /// public object? OldValue => GetOldValue(); /// /// Gets the new value of the property. /// - /// - /// The new value of the property or if the - /// property previously had no value. - /// public object? NewValue => GetNewValue(); /// @@ -58,6 +51,20 @@ namespace Avalonia /// public BindingPriority Priority { get; private set; } + /// + /// Gets a value indicating whether the change represents a change to the effective value of + /// the property. + /// + /// + /// This will usually be true, except in + /// + /// which recieves notifications for all changes to property values, whether a value with a higher + /// priority is present or not. When this property is false, the change that is being signalled + /// has not resulted in a change to the property value on the object. + /// + public bool IsEffectiveValueChange { get; private set; } + + internal void MarkNonEffectiveValue() => IsEffectiveValueChange = false; protected abstract AvaloniaProperty GetProperty(); protected abstract object? GetOldValue(); protected abstract object? GetNewValue(); diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs index fca32b4ffc..054bf93b3a 100644 --- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs @@ -1,4 +1,3 @@ -using System; using Avalonia.Data; #nullable enable @@ -6,7 +5,7 @@ using Avalonia.Data; namespace Avalonia { /// - /// Provides information for a avalonia property change. + /// Provides information for an Avalonia property change. /// public class AvaloniaPropertyChangedEventArgs : AvaloniaPropertyChangedEventArgs { @@ -42,19 +41,28 @@ namespace Avalonia /// /// Gets the old value of the property. /// - /// - /// The old value of the property. - /// + /// + /// When is true, returns the + /// old value of the property on the object. + /// When is false, returns + /// . + /// public new Optional OldValue { get; private set; } /// /// Gets the new value of the property. /// - /// - /// The new value of the property. - /// + /// + /// When is true, returns the + /// value of the property on the object. + /// When is false returns the + /// changed value, or if the value was removed. + /// public new BindingValue NewValue { get; private set; } + internal void SetOldValue(Optional value) => OldValue = value; + internal void SetNewValue(BindingValue value) => NewValue = value; + protected override AvaloniaProperty GetProperty() => Property; protected override object? GetOldValue() => OldValue.GetValueOrDefault(AvaloniaProperty.UnsetValue); diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index d3b5277c53..0e65379abd 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -120,6 +120,11 @@ namespace Avalonia return o.GetValue(this); } + internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) + { + return o.GetValue(this); + } + /// internal override IDisposable? RouteSetValue( IAvaloniaObject o, diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index 867249bf0e..0452f77d4c 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -41,6 +41,19 @@ namespace Avalonia /// The value. T GetValue(DirectPropertyBase property); + /// + /// Gets an base value. + /// + /// The type of the property. + /// The property. + /// The maximum priority for the value. + /// + /// Gets the value of the property, if set on this object with a priority equal or lower to + /// , otherwise . Note that + /// this method does not return property values that come from inherited or default values. + /// + Optional GetBaseValue(StyledPropertyBase property, BindingPriority maxPriority); + /// /// Checks whether a is animating. /// diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs index 3249b31d66..0d563947e7 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs @@ -22,6 +22,7 @@ namespace Avalonia.PropertyStore private readonly IAvaloniaObject _owner; private IValueSink _sink; private IDisposable? _subscription; + private Optional _value; public BindingEntry( IAvaloniaObject owner, @@ -40,18 +41,21 @@ namespace Avalonia.PropertyStore public StyledPropertyBase Property { get; } public BindingPriority Priority { get; } public IObservable> Source { get; } - public Optional Value { get; private set; } - Optional IValue.Value => Value.ToObject(); - BindingPriority IValue.ValuePriority => Priority; + Optional IValue.GetValue() => _value.ToObject(); + + public Optional GetValue(BindingPriority maxPriority) + { + return Priority >= maxPriority ? _value : Optional.Empty; + } public void Dispose() { _subscription?.Dispose(); _subscription = null; - _sink.Completed(Property, this, Value); + _sink.Completed(Property, this, _value); } - public void OnCompleted() => _sink.Completed(Property, this, Value); + public void OnCompleted() => _sink.Completed(Property, this, _value); public void OnError(Exception error) { @@ -94,14 +98,14 @@ namespace Avalonia.PropertyStore return; } - var old = Value; + var old = _value; if (value.Type != BindingValueType.DataValidationError) { - Value = value.ToOptional(); + _value = value.ToOptional(); } - _sink.ValueChanged(Property, Priority, old, value); + _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(_owner, Property, old, value, Priority)); } } } diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs index aa054c46ff..46f6f9a137 100644 --- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs @@ -13,6 +13,7 @@ namespace Avalonia.PropertyStore internal class ConstantValueEntry : IPriorityValueEntry, IDisposable { private IValueSink _sink; + private Optional _value; public ConstantValueEntry( StyledPropertyBase property, @@ -21,18 +22,21 @@ namespace Avalonia.PropertyStore IValueSink sink) { Property = property; - Value = value; + _value = value; Priority = priority; _sink = sink; } public StyledPropertyBase Property { get; } public BindingPriority Priority { get; } - public Optional Value { get; } - Optional IValue.Value => Value.ToObject(); - BindingPriority IValue.ValuePriority => Priority; + Optional IValue.GetValue() => _value.ToObject(); - public void Dispose() => _sink.Completed(Property, this, Value); + public Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation) + { + return Priority >= maxPriority ? _value : Optional.Empty; + } + + public void Dispose() => _sink.Completed(Property, this, _value); public void Reparent(IValueSink sink) => _sink = sink; } } diff --git a/src/Avalonia.Base/PropertyStore/IValue.cs b/src/Avalonia.Base/PropertyStore/IValue.cs index 0ce7fb8308..249cfc360c 100644 --- a/src/Avalonia.Base/PropertyStore/IValue.cs +++ b/src/Avalonia.Base/PropertyStore/IValue.cs @@ -9,8 +9,8 @@ namespace Avalonia.PropertyStore /// internal interface IValue { - Optional Value { get; } - BindingPriority ValuePriority { get; } + Optional GetValue(); + BindingPriority Priority { get; } } /// @@ -19,6 +19,6 @@ namespace Avalonia.PropertyStore /// The property type. internal interface IValue : IValue { - new Optional Value { get; } + Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation); } } diff --git a/src/Avalonia.Base/PropertyStore/IValueSink.cs b/src/Avalonia.Base/PropertyStore/IValueSink.cs index 9012a985ac..3a1e9731d8 100644 --- a/src/Avalonia.Base/PropertyStore/IValueSink.cs +++ b/src/Avalonia.Base/PropertyStore/IValueSink.cs @@ -9,11 +9,7 @@ namespace Avalonia.PropertyStore /// internal interface IValueSink { - void ValueChanged( - StyledPropertyBase property, - BindingPriority priority, - Optional oldValue, - BindingValue newValue); + void ValueChanged(AvaloniaPropertyChangedEventArgs change); void Completed( StyledPropertyBase property, diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs index 22258390da..59c017bc09 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs @@ -14,9 +14,14 @@ namespace Avalonia.PropertyStore private T _value; public LocalValueEntry(T value) => _value = value; - public Optional Value => _value; - public BindingPriority ValuePriority => BindingPriority.LocalValue; - Optional IValue.Value => Value.ToObject(); + public BindingPriority Priority => BindingPriority.LocalValue; + Optional IValue.GetValue() => new Optional(_value); + + public Optional GetValue(BindingPriority maxPriority) + { + return BindingPriority.LocalValue >= maxPriority ? _value : Optional.Empty; + } + public void SetValue(T value) => _value = value; } } diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index affb20f334..5e223cad60 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Avalonia.Data; #nullable enable @@ -24,6 +25,7 @@ namespace Avalonia.PropertyStore private readonly List> _entries = new List>(); private readonly Func? _coerceValue; private Optional _localValue; + private Optional _value; public PriorityValue( IAvaloniaObject owner, @@ -50,11 +52,13 @@ namespace Avalonia.PropertyStore { existing.Reparent(this); _entries.Add(existing); + + var v = existing.GetValue(); - if (existing.Value.HasValue) + if (v.HasValue) { - Value = existing.Value; - ValuePriority = existing.Priority; + _value = v; + Priority = existing.Priority; } } @@ -65,18 +69,39 @@ namespace Avalonia.PropertyStore LocalValueEntry existing) : this(owner, property, sink) { - _localValue = existing.Value; - Value = _localValue; - ValuePriority = BindingPriority.LocalValue; + _value = _localValue = existing.GetValue(BindingPriority.LocalValue); + Priority = BindingPriority.LocalValue; } public StyledPropertyBase Property { get; } - public Optional Value { get; private set; } - public BindingPriority ValuePriority { get; private set; } + public BindingPriority Priority { get; private set; } = BindingPriority.Unset; public IReadOnlyList> Entries => _entries; - Optional IValue.Value => Value.ToObject(); + Optional IValue.GetValue() => _value.ToObject(); + + public void ClearLocalValue() + { + UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( + _owner, + Property, + default, + default, + BindingPriority.LocalValue)); + } + + public Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation) + { + if (Priority == BindingPriority.Unset) + { + return default; + } - public void ClearLocalValue() => UpdateEffectiveValue(); + if (Priority >= maxPriority) + { + return _value; + } + + return CalculateValue(maxPriority).Item1; + } public IDisposable? SetValue(T value, BindingPriority priority) { @@ -94,7 +119,13 @@ namespace Avalonia.PropertyStore result = entry; } - UpdateEffectiveValue(); + UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( + _owner, + Property, + default, + value, + priority)); + return result; } @@ -106,20 +137,19 @@ namespace Avalonia.PropertyStore return binding; } - public void CoerceValue() => UpdateEffectiveValue(); + public void CoerceValue() => UpdateEffectiveValue(null); - void IValueSink.ValueChanged( - StyledPropertyBase property, - BindingPriority priority, - Optional oldValue, - BindingValue newValue) + void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change) { - if (priority == BindingPriority.LocalValue) + if (change.Priority == BindingPriority.LocalValue) { _localValue = default; } - UpdateEffectiveValue(); + if (change is AvaloniaPropertyChangedEventArgs c) + { + UpdateEffectiveValue(c); + } } void IValueSink.Completed( @@ -128,7 +158,16 @@ namespace Avalonia.PropertyStore Optional oldValue) { _entries.Remove((IPriorityValueEntry)entry); - UpdateEffectiveValue(); + + if (oldValue is Optional o) + { + UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( + _owner, + Property, + o, + default, + entry.Priority)); + } } private int FindInsertPoint(BindingPriority priority) @@ -147,53 +186,73 @@ namespace Avalonia.PropertyStore return result; } - private void UpdateEffectiveValue() + public (Optional, BindingPriority) CalculateValue(BindingPriority maxPriority) { var reachedLocalValues = false; - var value = default(Optional); - if (_entries.Count > 0) + for (var i = _entries.Count - 1; i >= 0; --i) { - for (var i = _entries.Count - 1; i >= 0; --i) + var entry = _entries[i]; + + if (entry.Priority < maxPriority) + { + continue; + } + + if (!reachedLocalValues && + entry.Priority >= BindingPriority.LocalValue && + maxPriority <= BindingPriority.LocalValue && + _localValue.HasValue) + { + return (_localValue, BindingPriority.LocalValue); + } + + var entryValue = entry.GetValue(); + + if (entryValue.HasValue) { - var entry = _entries[i]; - - if (!reachedLocalValues && entry.Priority >= BindingPriority.LocalValue) - { - reachedLocalValues = true; - - if (_localValue.HasValue) - { - value = _localValue; - ValuePriority = BindingPriority.LocalValue; - break; - } - } - - if (entry.Value.HasValue) - { - value = entry.Value; - ValuePriority = entry.Priority; - break; - } + return (entryValue, entry.Priority); } } - else if (_localValue.HasValue) + + if (!reachedLocalValues && + maxPriority <= BindingPriority.LocalValue && + _localValue.HasValue) { - value = _localValue; - ValuePriority = BindingPriority.LocalValue; + return (_localValue, BindingPriority.LocalValue); } + return (default, BindingPriority.Unset); + } + + private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs? change) + { + var (value, priority) = CalculateValue(BindingPriority.Animation); + if (value.HasValue && _coerceValue != null) { value = _coerceValue(_owner, value.Value); } - if (value != Value) + Priority = priority; + + if (value != _value) + { + var old = _value; + _value = value; + + _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + _owner, + Property, + old, + value, + Priority)); + } + else if (change is object) { - var old = Value; - Value = value; - _sink.ValueChanged(Property, ValuePriority, old, value); + change.MarkNonEffectiveValue(); + change.SetOldValue(default); + _sink.ValueChanged(change); } } } diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index 1f88bfb2aa..3e92c3bdf7 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -197,6 +197,13 @@ namespace Avalonia return o.GetValue(this); } + /// + internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) + { + var value = o.GetBaseValue(this, maxPriority); + return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue; + } + /// internal override IDisposable RouteSetValue( IAvaloniaObject o, diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 104c06de0f..05e66f2e0a 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -37,7 +37,7 @@ namespace Avalonia { if (_values.TryGetValue(property, out var slot)) { - return slot.ValuePriority < BindingPriority.LocalValue; + return slot.Priority < BindingPriority.LocalValue; } return false; @@ -47,21 +47,24 @@ namespace Avalonia { if (_values.TryGetValue(property, out var slot)) { - return slot.Value.HasValue; + return slot.GetValue().HasValue; } return false; } - public bool TryGetValue(StyledPropertyBase property, out T value) + public bool TryGetValue( + StyledPropertyBase property, + BindingPriority maxPriority, + out T value) { if (_values.TryGetValue(property, out var slot)) { - var v = (IValue)slot; + var v = ((IValue)slot).GetValue(maxPriority); - if (v.Value.HasValue) + if (v.HasValue) { - value = v.Value.Value; + value = v.Value; return true; } } @@ -90,17 +93,22 @@ namespace Avalonia _values.AddValue(property, entry); result = entry.SetValue(value, priority); } - else if (priority == BindingPriority.LocalValue) - { - _values.AddValue(property, new LocalValueEntry(value)); - _sink.ValueChanged(property, priority, default, value); - } else { - var entry = new ConstantValueEntry(property, value, priority, this); - _values.AddValue(property, entry); - _sink.ValueChanged(property, priority, default, value); - result = entry; + var change = new AvaloniaPropertyChangedEventArgs(_owner, property, default, value, priority); + + if (priority == BindingPriority.LocalValue) + { + _values.AddValue(property, new LocalValueEntry(value)); + _sink.ValueChanged(change); + } + else + { + var entry = new ConstantValueEntry(property, value, priority, this); + _values.AddValue(property, entry); + _sink.ValueChanged(change); + result = entry; + } } return result; @@ -149,13 +157,14 @@ namespace Avalonia if (remove) { - var old = TryGetValue(property, out var value) ? value : default; + var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default; _values.Remove(property); - _sink.ValueChanged( + _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + _owner, property, - BindingPriority.Unset, old, - BindingValue.Unset); + default, + BindingPriority.Unset)); } } } @@ -176,23 +185,20 @@ namespace Avalonia { if (_values.TryGetValue(property, out var slot)) { + var slotValue = slot.GetValue(); return new Diagnostics.AvaloniaPropertyValue( property, - slot.Value.HasValue ? slot.Value.Value : AvaloniaProperty.UnsetValue, - slot.ValuePriority, + slotValue.HasValue ? slotValue.Value : AvaloniaProperty.UnsetValue, + slot.Priority, null); } return null; } - void IValueSink.ValueChanged( - StyledPropertyBase property, - BindingPriority priority, - Optional oldValue, - BindingValue newValue) + void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change) { - _sink.ValueChanged(property, priority, oldValue, newValue); + _sink.ValueChanged(change); } void IValueSink.Completed( @@ -232,9 +238,14 @@ namespace Avalonia { if (priority == BindingPriority.LocalValue) { - var old = l.Value; + var old = l.GetValue(BindingPriority.LocalValue); l.SetValue(value); - _sink.ValueChanged(property, priority, old, value); + _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + _owner, + property, + old, + value, + priority)); } else { diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 844316741a..3fa773b71f 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -767,7 +767,7 @@ namespace Avalonia.Controls /// /// ItemsProperty property changed handler. /// - /// AvaloniaPropertyChangedEventArgs. + /// AvaloniaPropertyChangedEventArgsdEventArgs. private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e) { if (!_areHandlersSuspended) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 8143bb1cf9..c171217642 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -313,17 +313,13 @@ namespace Avalonia.Controls IsPressed = false; } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == IsPressedProperty) + if (change.Property == IsPressedProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault()); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index 2ac9319478..0717ab800a 100644 --- a/src/Avalonia.Controls/ButtonSpinner.cs +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -205,17 +205,13 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == ButtonSpinnerLocationProperty) + if (change.Property == ButtonSpinnerLocationProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault()); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs index b4e4ad1452..865b00cefc 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -512,17 +512,13 @@ namespace Avalonia.Controls base.OnTemplateApplied(e); } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == SelectedDateProperty) + if (change.Property == SelectedDateProperty) { - DataValidationErrors.SetError(this, newValue.Error); + DataValidationErrors.SetError(this, change.NewValue.Error); } } diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index e42d3ec1e5..43882b70c8 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -78,17 +78,13 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == ExpandDirectionProperty) + if (change.Property == ExpandDirectionProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault()); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index f1d8fe6763..62868f740a 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -138,17 +138,13 @@ namespace Avalonia.Controls.Notifications notificationControl.Close(); } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == PositionProperty) + if (change.Property == PositionProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault()); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index d48a9316e8..5ed75c97cf 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -123,24 +123,20 @@ namespace Avalonia.Controls.Primitives } } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == OrientationProperty) + if (change.Property == OrientationProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault()); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); } else { - if (property == MinimumProperty || - property == MaximumProperty || - property == ViewportSizeProperty || - property == VisibilityProperty) + if (change.Property == MinimumProperty || + change.Property == MaximumProperty || + change.Property == ViewportSizeProperty || + change.Property == VisibilityProperty) { UpdateIsVisible(); } diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index 1e02d70fff..e104a8a664 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -280,17 +280,13 @@ namespace Avalonia.Controls.Primitives return arrangeSize; } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == OrientationProperty) + if (change.Property == OrientationProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault()); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 361a82e49c..78adf07e03 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -83,21 +83,17 @@ namespace Avalonia.Controls return base.ArrangeOverride(finalSize); } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == IsIndeterminateProperty) + if (change.Property == IsIndeterminateProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault(), null); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault(), null); } - else if (property == OrientationProperty) + else if (change.Property == OrientationProperty) { - UpdatePseudoClasses(null, newValue.GetValueOrDefault()); + UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 086599d0bb..24a4dc9ea6 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -375,11 +375,11 @@ namespace Avalonia.Controls _viewportManager.ResetScrollers(); } - protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (property == ItemsProperty) + if (change.Property == ItemsProperty) { - var newEnumerable = newValue.GetValueOrDefault(); + var newEnumerable = change.NewValue.GetValueOrDefault(); var newDataSource = newEnumerable as ItemsSourceView; if (newEnumerable != null && newDataSource == null) { @@ -388,24 +388,28 @@ namespace Avalonia.Controls OnDataSourcePropertyChanged(ItemsSourceView, newDataSource); } - else if (property == ItemTemplateProperty) + else if (change.Property == ItemTemplateProperty) { - OnItemTemplateChanged(oldValue.GetValueOrDefault(), newValue.GetValueOrDefault()); + OnItemTemplateChanged( + change.OldValue.GetValueOrDefault(), + change.NewValue.GetValueOrDefault()); } - else if (property == LayoutProperty) + else if (change.Property == LayoutProperty) { - OnLayoutChanged(oldValue.GetValueOrDefault(), newValue.GetValueOrDefault()); + OnLayoutChanged( + change.OldValue.GetValueOrDefault(), + change.NewValue.GetValueOrDefault()); } - else if (property == HorizontalCacheLengthProperty) + else if (change.Property == HorizontalCacheLengthProperty) { - _viewportManager.HorizontalCacheLength = newValue.GetValueOrDefault(); + _viewportManager.HorizontalCacheLength = change.NewValue.GetValueOrDefault(); } - else if (property == VerticalCacheLengthProperty) + else if (change.Property == VerticalCacheLengthProperty) { - _viewportManager.VerticalCacheLength = newValue.GetValueOrDefault(); + _viewportManager.VerticalCacheLength = change.NewValue.GetValueOrDefault(); } - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); } internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle) diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index b883a76d1b..3862e87723 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -134,17 +134,13 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == OrientationProperty) + if (change.Property == OrientationProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault()); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index dd00b850fe..0420356257 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -645,19 +645,15 @@ namespace Avalonia.Controls /// protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e); - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (property == SystemDecorationsProperty) + if (change.Property == SystemDecorationsProperty) { - var typedNewValue = newValue.GetValueOrDefault(); + var typedNewValue = change.NewValue.GetValueOrDefault(); PlatformImpl?.SetSystemDecorations(typedNewValue); - var o = oldValue.GetValueOrDefault() == SystemDecorations.Full; + var o = change.OldValue.GetValueOrDefault() == SystemDecorations.Full; var n = typedNewValue == SystemDecorations.Full; if (o != n) diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 20c775f965..407b28b665 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -526,17 +526,17 @@ namespace Avalonia.Input { } - protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == IsFocusedProperty) + if (change.Property == IsFocusedProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault(), null); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault(), null); } - else if (property == IsPointerOverProperty) + else if (change.Property == IsPointerOverProperty) { - UpdatePseudoClasses(null, newValue.GetValueOrDefault()); + UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index 9b8eb4814e..4eca8df7fb 100644 --- a/src/Avalonia.Layout/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -296,11 +296,11 @@ namespace Avalonia.Layout InvalidateLayout(); } - protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (property == OrientationProperty) + if (change.Property == OrientationProperty) { - var orientation = newValue.GetValueOrDefault(); + var orientation = change.NewValue.GetValueOrDefault(); //Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation. //Horizontal Orientation means we have a Horizontal ScrollOrientation. diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs index ee9cff4a01..84735cf66b 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -463,45 +463,44 @@ namespace Avalonia.Layout gridState.ClearElementOnDataSourceChange(context, args); } - protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (property == OrientationProperty) + if (change.Property == OrientationProperty) { - var orientation = newValue.GetValueOrDefault(); + var orientation = change.NewValue.GetValueOrDefault(); //Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation. //i.e. the properties are the inverse of each other. var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal; _orientation.ScrollOrientation = scrollOrientation; } - else if (property == MinColumnSpacingProperty) + else if (change.Property == MinColumnSpacingProperty) { - _minColumnSpacing = newValue.GetValueOrDefault(); + _minColumnSpacing = change.NewValue.GetValueOrDefault(); } - else if (property == MinRowSpacingProperty) + else if (change.Property == MinRowSpacingProperty) { - _minRowSpacing = newValue.GetValueOrDefault(); + _minRowSpacing = change.NewValue.GetValueOrDefault(); } - else if (property == ItemsJustificationProperty) + else if (change.Property == ItemsJustificationProperty) { - _itemsJustification = newValue.GetValueOrDefault(); - ; + _itemsJustification = change.NewValue.GetValueOrDefault(); } - else if (property == ItemsStretchProperty) + else if (change.Property == ItemsStretchProperty) { - _itemsStretch = newValue.GetValueOrDefault(); + _itemsStretch = change.NewValue.GetValueOrDefault(); } - else if (property == MinItemWidthProperty) + else if (change.Property == MinItemWidthProperty) { - _minItemWidth = newValue.GetValueOrDefault(); + _minItemWidth = change.NewValue.GetValueOrDefault(); } - else if (property == MinItemHeightProperty) + else if (change.Property == MinItemHeightProperty) { - _minItemHeight = newValue.GetValueOrDefault(); + _minItemHeight = change.NewValue.GetValueOrDefault(); } - else if (property == MaximumRowsOrColumnsProperty) + else if (change.Property == MaximumRowsOrColumnsProperty) { - _maximumRowsOrColumns = newValue.GetValueOrDefault(); + _maximumRowsOrColumns = change.NewValue.GetValueOrDefault(); } InvalidateLayout(); diff --git a/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs b/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs index 1400bc2ac3..f975862892 100644 --- a/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs +++ b/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs @@ -160,7 +160,7 @@ namespace Avalonia.Styling private void ConvertAndPublishNext(object? value) { - _value = value is T v ? v : BindingValue.FromUntyped(value).Convert(); + _value = value is T v ? v : BindingValue.FromUntyped(value); if (_isActive) { diff --git a/src/Avalonia.Visuals/Media/DrawingImage.cs b/src/Avalonia.Visuals/Media/DrawingImage.cs index 57939bab24..56c883014a 100644 --- a/src/Avalonia.Visuals/Media/DrawingImage.cs +++ b/src/Avalonia.Visuals/Media/DrawingImage.cs @@ -63,15 +63,11 @@ namespace Avalonia.Media } /// - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == DrawingProperty) + if (change.Property == DrawingProperty) { RaiseInvalidated(EventArgs.Empty); } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs index 5da1f95646..6bd29a1577 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Subjects; +using Avalonia.Data; using Xunit; namespace Avalonia.Base.UnitTests @@ -63,6 +64,56 @@ namespace Avalonia.Base.UnitTests Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); } + [Fact] + public void GetBaseValue_LocalValue_Ignores_Default_Value() + { + var target = new Class3(); + + target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation); + Assert.False(target.GetBaseValue(Class1.FooProperty, BindingPriority.LocalValue).HasValue); + } + + [Fact] + public void GetBaseValue_LocalValue_Returns_Local_Value() + { + var target = new Class3(); + + target.SetValue(Class1.FooProperty, "local"); + target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation); + Assert.Equal("local", target.GetBaseValue(Class1.FooProperty, BindingPriority.LocalValue).Value); + } + + [Fact] + public void GetBaseValue_LocalValue_Returns_Style_Value() + { + var target = new Class3(); + + target.SetValue(Class1.FooProperty, "style", BindingPriority.Style); + target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation); + Assert.Equal("style", target.GetBaseValue(Class1.FooProperty, BindingPriority.LocalValue).Value); + } + + [Fact] + public void GetBaseValue_Style_Ignores_LocalValue_Animated_Value() + { + var target = new Class3(); + + target.Bind(Class1.FooProperty, new BehaviorSubject("animated"), BindingPriority.Animation); + target.SetValue(Class1.FooProperty, "local"); + Assert.False(target.GetBaseValue(Class1.FooProperty, BindingPriority.Style).HasValue); + } + + [Fact] + public void GetBaseValue_Style_Returns_Style_Value() + { + var target = new Class3(); + + target.SetValue(Class1.FooProperty, "local"); + target.SetValue(Class1.FooProperty, "style", BindingPriority.Style); + target.Bind(Class1.FooProperty, new BehaviorSubject("animated"), BindingPriority.Animation); + Assert.Equal("style", target.GetBaseValue(Class1.FooProperty, BindingPriority.Style)); + } + private class Class1 : AvaloniaObject { public static readonly StyledProperty FooProperty = diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs new file mode 100644 index 0000000000..e8fc3f9f40 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs @@ -0,0 +1,142 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using Avalonia.Data; +using Xunit; + +namespace Avalonia.Base.UnitTests +{ + public class AvaloniaObjectTests_OnPropertyChanged + { + [Fact] + public void OnPropertyChangedCore_Is_Called_On_Property_Change() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "newvalue"); + + Assert.Equal(1, target.CoreChanges.Count); + + var change = (AvaloniaPropertyChangedEventArgs)target.CoreChanges[0]; + + Assert.Equal("newvalue", change.NewValue.Value); + Assert.Equal("foodefault", change.OldValue.Value); + Assert.Equal(BindingPriority.LocalValue, change.Priority); + Assert.True(change.IsEffectiveValueChange); + } + + [Fact] + public void OnPropertyChangedCore_Is_Called_On_Non_Effective_Property_Value_Change() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "newvalue"); + target.SetValue(Class1.FooProperty, "styled", BindingPriority.Style); + + Assert.Equal(2, target.CoreChanges.Count); + + var change = (AvaloniaPropertyChangedEventArgs)target.CoreChanges[1]; + + Assert.Equal("styled", change.NewValue.Value); + Assert.False(change.OldValue.HasValue); + Assert.Equal(BindingPriority.Style, change.Priority); + Assert.False(change.IsEffectiveValueChange); + } + + [Fact] + public void OnPropertyChangedCore_Is_Called_On_All_Binding_Property_Changes() + { + var target = new Class1(); + var style = new Subject>(); + var animation = new Subject>(); + var templatedParent = new Subject>(); + + target.Bind(Class1.FooProperty, style, BindingPriority.Style); + target.Bind(Class1.FooProperty, animation, BindingPriority.Animation); + target.Bind(Class1.FooProperty, templatedParent, BindingPriority.TemplatedParent); + + style.OnNext("style1"); + templatedParent.OnNext("tp1"); + animation.OnNext("a1"); + templatedParent.OnNext("tp2"); + templatedParent.OnCompleted(); + animation.OnNext("a2"); + style.OnNext("style2"); + style.OnCompleted(); + animation.OnCompleted(); + + var changes = target.CoreChanges.Cast>(); + + Assert.Equal( + new[] { true, true, true, false, false, true, false, false, true }, + changes.Select(x => x.IsEffectiveValueChange).ToList()); + Assert.Equal( + new[] { "style1", "tp1", "a1", "tp2", "$unset", "a2", "style2", "$unset", "foodefault" }, + changes.Select(x => x.NewValue.GetValueOrDefault("$unset")).ToList()); + Assert.Equal( + new[] { "foodefault", "style1", "tp1", "$unset", "$unset", "a1", "$unset", "$unset", "a2" }, + changes.Select(x => x.OldValue.GetValueOrDefault("$unset")).ToList()); + } + + [Fact] + public void OnPropertyChanged_Is_Called_Only_For_Effective_Value_Changes() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "newvalue"); + target.SetValue(Class1.FooProperty, "styled", BindingPriority.Style); + + Assert.Equal(1, target.Changes.Count); + Assert.Equal(2, target.CoreChanges.Count); + } + + private class Class1 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register("Foo", "foodefault"); + + public Class1() + { + Changes = new List(); + CoreChanges = new List(); + } + + public List Changes { get; } + public List CoreChanges { get; } + + protected override void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) + { + CoreChanges.Add(Clone(change)); + base.OnPropertyChangedCore(change); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + Changes.Add(Clone(change)); + base.OnPropertyChanged(change); + } + + private static AvaloniaPropertyChangedEventArgs Clone(AvaloniaPropertyChangedEventArgs change) + { + var result = new AvaloniaPropertyChangedEventArgs( + change.Sender, + change.Property, + change.OldValue, + change.NewValue, + change.Priority); + + if (!change.IsEffectiveValueChange) + { + result.MarkNonEffectiveValue(); + } + + return result; + } + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 941dd9bd98..3819e715f3 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.Data; using Avalonia.Utilities; using Xunit; @@ -88,6 +89,19 @@ namespace Avalonia.Base.UnitTests Assert.Equal("newvalue", value); } + [Fact] + public void Changed_Observable_Fired_Only_On_Effective_Value_Change() + { + var target = new Class1(); + var result = new List(); + + Class1.FooProperty.Changed.Subscribe(x => result.Add((string)x.NewValue)); + target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation); + target.SetValue(Class1.FooProperty, "local"); + + Assert.Equal(new[] { "animated" }, result); + } + [Fact] public void Property_Equals_Should_Handle_Null() { @@ -144,6 +158,11 @@ namespace Avalonia.Base.UnitTests throw new NotImplementedException(); } + internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) + { + throw new NotImplementedException(); + } + internal override void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent) { throw new NotImplementedException(); diff --git a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs index 5e69b8490d..0caa984a22 100644 --- a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs +++ b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs @@ -10,7 +10,7 @@ namespace Avalonia.Base.UnitTests { public class PriorityValueTests { - private static readonly IValueSink NullSink = Mock.Of(); + private static readonly IValueSink NullSink = new MockSink(); private static readonly IAvaloniaObject Owner = Mock.Of(); private static readonly StyledProperty TestProperty = new StyledProperty( "Test", @@ -30,8 +30,28 @@ namespace Avalonia.Base.UnitTests BindingPriority.StyleTrigger, NullSink)); - Assert.Equal("1", target.Value.Value); - Assert.Equal(BindingPriority.StyleTrigger, target.ValuePriority); + Assert.Equal("1", target.GetValue().Value); + Assert.Equal(BindingPriority.StyleTrigger, target.Priority); + } + + [Fact] + public void GetValue_Should_Respect_MaxPriority() + { + var target = new PriorityValue( + Owner, + TestProperty, + NullSink); + + target.SetValue("animation", BindingPriority.Animation); + target.SetValue("local", BindingPriority.LocalValue); + target.SetValue("styletrigger", BindingPriority.StyleTrigger); + target.SetValue("style", BindingPriority.Style); + + Assert.Equal("animation", target.GetValue(BindingPriority.Animation)); + Assert.Equal("local", target.GetValue(BindingPriority.LocalValue)); + Assert.Equal("styletrigger", target.GetValue(BindingPriority.StyleTrigger)); + Assert.Equal("style", target.GetValue(BindingPriority.TemplatedParent)); + Assert.Equal("style", target.GetValue(BindingPriority.Style)); } [Fact] @@ -61,12 +81,31 @@ namespace Avalonia.Base.UnitTests var result = target.Entries .OfType>() - .Select(x => x.Value.Value) + .Select(x => x.GetValue().Value) .ToList(); Assert.Equal(new[] { "1", "2" }, result); } + [Fact] + public void Priority_Should_Be_Set() + { + var target = new PriorityValue( + Owner, + TestProperty, + NullSink); + + Assert.Equal(BindingPriority.Unset, target.Priority); + target.SetValue("style", BindingPriority.Style); + Assert.Equal(BindingPriority.Style, target.Priority); + target.SetValue("local", BindingPriority.LocalValue); + Assert.Equal(BindingPriority.LocalValue, target.Priority); + target.SetValue("animation", BindingPriority.Animation); + Assert.Equal(BindingPriority.Animation, target.Priority); + target.SetValue("local2", BindingPriority.LocalValue); + Assert.Equal(BindingPriority.Animation, target.Priority); + } + [Fact] public void Binding_With_Same_Priority_Should_Be_Appended() { @@ -184,7 +223,7 @@ namespace Avalonia.Base.UnitTests target.AddBinding(source2, BindingPriority.Style).Start(); target.AddBinding(source3, BindingPriority.Style).Start(); - Assert.Equal("1", target.Value.Value); + Assert.Equal("1", target.GetValue().Value); } [Fact] @@ -196,7 +235,7 @@ namespace Avalonia.Base.UnitTests target.AddBinding(source1, BindingPriority.LocalValue).Start(); target.SetValue("2", BindingPriority.LocalValue); - Assert.Equal("2", target.Value.Value); + Assert.Equal("2", target.GetValue().Value); } [Fact] @@ -208,7 +247,7 @@ namespace Avalonia.Base.UnitTests target.AddBinding(source1, BindingPriority.Style).Start(); target.SetValue("2", BindingPriority.LocalValue); - Assert.Equal("2", target.Value.Value); + Assert.Equal("2", target.GetValue().Value); } [Fact] @@ -220,7 +259,39 @@ namespace Avalonia.Base.UnitTests target.AddBinding(source1, BindingPriority.Animation).Start(); target.SetValue("2", BindingPriority.LocalValue); - Assert.Equal("1", target.Value.Value); + Assert.Equal("1", target.GetValue().Value); + } + + [Fact] + public void NonAnimated_Value_Should_Be_Correct_1() + { + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); + var source2 = new Source("2"); + var source3 = new Source("3"); + + target.AddBinding(source1, BindingPriority.LocalValue).Start(); + target.AddBinding(source2, BindingPriority.Style).Start(); + target.AddBinding(source3, BindingPriority.Animation).Start(); + + Assert.Equal("3", target.GetValue().Value); + Assert.Equal("1", target.GetValue(BindingPriority.LocalValue).Value); + } + + [Fact] + public void NonAnimated_Value_Should_Be_Correct_2() + { + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); + var source2 = new Source("2"); + var source3 = new Source("3"); + + target.AddBinding(source1, BindingPriority.Animation).Start(); + target.AddBinding(source2, BindingPriority.Style).Start(); + target.AddBinding(source3, BindingPriority.Style).Start(); + + Assert.Equal("1", target.GetValue().Value); + Assert.Equal("3", target.GetValue(BindingPriority.LocalValue).Value); } private class Source : IObservable> @@ -239,5 +310,16 @@ namespace Avalonia.Base.UnitTests public void OnCompleted() => _observer.OnCompleted(); } + + private class MockSink : IValueSink + { + public void Completed(StyledPropertyBase property, IPriorityValueEntry entry, Optional oldValue) + { + } + + public void ValueChanged(AvaloniaPropertyChangedEventArgs change) + { + } + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs index 3d0cb88c8a..cb37dea220 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs @@ -18,10 +18,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml base.OnAttachedToLogicalTree(e); } - protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - Order.Add($"Property {property.Name} Changed"); - base.OnPropertyChanged(property, oldValue, newValue, priority); + Order.Add($"Property {change.Property.Name} Changed"); + base.OnPropertyChanged(change); } void ISupportInitialize.BeginInit() diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs index ab30d91971..52552f0bee 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs @@ -36,7 +36,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "https://github.com/moq/moq4/issues/988")] public void AddDirty_With_RenderTransform_Call_RenderRoot_Invalidate() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -59,7 +59,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "https://github.com/moq/moq4/issues/988")] public void AddDirty_For_Child_Moved_Should_Invalidate_Previous_Bounds() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -111,7 +111,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "https://github.com/moq/moq4/issues/988")] public void Should_Render_Child_In_Parent_With_RenderTransform() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -145,7 +145,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "https://github.com/moq/moq4/issues/988")] public void Should_Render_Child_In_Parent_With_RenderTransform2() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) From 95c04bcbec35b570684cfe13d29d898b77727a0a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Apr 2020 09:39:58 +0200 Subject: [PATCH 02/44] Fix bug in BindingValue. Wrong `BindingValueType` was being selected. --- src/Avalonia.Base/Data/BindingValue.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index cecdd33e7b..9aac1bacba 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -348,8 +348,8 @@ namespace Avalonia.Data return new BindingValue( fallbackValue.HasValue ? - BindingValueType.DataValidationError : - BindingValueType.DataValidationErrorWithFallback, + BindingValueType.DataValidationErrorWithFallback : + BindingValueType.DataValidationError, fallbackValue.HasValue ? fallbackValue.Value : default, e); } From c872cc005d37529017f7311d5c180708e73ad9b2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Apr 2020 15:15:31 +0200 Subject: [PATCH 03/44] Only call property notify on effective value change. --- src/Avalonia.Base/AvaloniaObject.cs | 10 ++++++++-- .../AvaloniaPropertyTests.cs | 20 ++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 546ab78e31..f387d7e0b6 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -716,7 +716,10 @@ namespace Avalonia { VerifyAccess(); - change.Property.Notifying?.Invoke(this, true); + if (change.IsEffectiveValueChange) + { + change.Property.Notifying?.Invoke(this, true); + } try { @@ -747,7 +750,10 @@ namespace Avalonia } finally { - change.Property.Notifying?.Invoke(this, false); + if (change.IsEffectiveValueChange) + { + change.Property.Notifying?.Invoke(this, false); + } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 3819e715f3..d7f927372e 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -102,6 +102,17 @@ namespace Avalonia.Base.UnitTests Assert.Equal(new[] { "animated" }, result); } + [Fact] + public void Notify_Fired_Only_On_Effective_Value_Change() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation); + target.SetValue(Class1.FooProperty, "local"); + + Assert.Equal(2, target.NotifyCount); + } + [Fact] public void Property_Equals_Should_Handle_Null() { @@ -180,7 +191,14 @@ namespace Avalonia.Base.UnitTests private class Class1 : AvaloniaObject { public static readonly StyledProperty FooProperty = - AvaloniaProperty.Register("Foo", "default"); + AvaloniaProperty.Register("Foo", "default", notifying: FooNotifying); + + public int NotifyCount { get; private set; } + + private static void FooNotifying(IAvaloniaObject o, bool n) + { + ++((Class1)o).NotifyCount; + } } private class Class2 : Class1 From ba75735848cff6287402ec4e9bd8ade95e72e8e4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 May 2020 16:55:09 -0300 Subject: [PATCH 04/44] decode bitmaps at a specified size. --- src/Avalonia.Visuals/Media/Imaging/Bitmap.cs | 12 ++++++++++ .../Media/Imaging/BitmapDecodeOptions.cs | 11 +++++++++ .../Platform/IPlatformRenderInterface.cs | 5 ++++ src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 19 +-------------- .../Avalonia.Skia/PlatformRenderInterface.cs | 23 +++++++++++++++++++ src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs | 18 +++++++++++++++ .../Avalonia.Direct2D1/Direct2D1Platform.cs | 11 +++++++++ 7 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 src/Avalonia.Visuals/Media/Imaging/BitmapDecodeOptions.cs diff --git a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs index 42ee35133e..408a93eae7 100644 --- a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs @@ -31,6 +31,18 @@ namespace Avalonia.Media.Imaging PlatformImpl = RefCountable.Create(factory.LoadBitmap(stream)); } + public Bitmap(Stream stream, BitmapDecodeOptions decodeOptions) + { + IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); + PlatformImpl = RefCountable.Create(factory.LoadBitmap(stream, decodeOptions)); + } + + public Bitmap(string file, BitmapDecodeOptions decodeOptions) + { + IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); + PlatformImpl = RefCountable.Create(factory.LoadBitmap(file, decodeOptions)); + } + /// /// Initializes a new instance of the class. /// diff --git a/src/Avalonia.Visuals/Media/Imaging/BitmapDecodeOptions.cs b/src/Avalonia.Visuals/Media/Imaging/BitmapDecodeOptions.cs new file mode 100644 index 0000000000..ec9d823bb4 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Imaging/BitmapDecodeOptions.cs @@ -0,0 +1,11 @@ +using Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Media.Imaging +{ + public struct BitmapDecodeOptions + { + public PixelSize DecodePixelSize { get; set; } + + public BitmapInterpolationMode InterpolationMode { get; set; } + } +} diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index bd569fe841..86662a3584 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using Avalonia.Media; +using Avalonia.Media.Imaging; namespace Avalonia.Platform { @@ -84,6 +85,10 @@ namespace Avalonia.Platform /// An . IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? format = null); + IBitmapImpl LoadBitmap(string fileName, BitmapDecodeOptions decodeOptions); + + IBitmapImpl LoadBitmap(Stream stream, BitmapDecodeOptions decodeOptions); + /// /// Loads a bitmap implementation from a file.. /// diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 9f99ed3cef..6472338734 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -123,29 +123,12 @@ namespace Avalonia.Skia Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)) }) { - paint.FilterQuality = GetInterpolationMode(bitmapInterpolationMode); + paint.FilterQuality = bitmapInterpolationMode.ToSKFilterQuality(); drawableImage.Draw(this, s, d, paint); } } - private static SKFilterQuality GetInterpolationMode(BitmapInterpolationMode interpolationMode) - { - switch (interpolationMode) - { - case BitmapInterpolationMode.LowQuality: - return SKFilterQuality.Low; - case BitmapInterpolationMode.MediumQuality: - return SKFilterQuality.Medium; - case BitmapInterpolationMode.HighQuality: - return SKFilterQuality.High; - case BitmapInterpolationMode.Default: - return SKFilterQuality.None; - default: - throw new ArgumentOutOfRangeException(nameof(interpolationMode), interpolationMode, null); - } - } - /// public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index f6d77e1045..b96a65186e 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; +using Avalonia.Media.Imaging; using Avalonia.OpenGL; using Avalonia.Platform; using SkiaSharp; @@ -77,6 +78,28 @@ namespace Avalonia.Skia return new StreamGeometryImpl(); } + public IBitmapImpl LoadBitmap(string fileName, BitmapDecodeOptions decodeOptions) + { + using (var stream = File.OpenRead(fileName)) + { + return LoadBitmap(stream, decodeOptions); + } + } + + public unsafe IBitmapImpl LoadBitmap(Stream stream, BitmapDecodeOptions decodeOptions) + { + var skBitmap = SKBitmap.Decode(stream); + + skBitmap = skBitmap.Resize(new SKImageInfo(decodeOptions.DecodePixelSize.Width, decodeOptions.DecodePixelSize.Height), decodeOptions.InterpolationMode.ToSKFilterQuality()); + + fixed (byte* p = skBitmap.Bytes) + { + IntPtr ptr = (IntPtr)p; + + return LoadBitmap(PixelFormat.Bgra8888, ptr, new PixelSize(skBitmap.Width, skBitmap.Height), new Vector(96, 96), skBitmap.RowBytes); + } + } + /// public IBitmapImpl LoadBitmap(Stream stream) { diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index 1dd2310475..c706d5c1aa 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -1,12 +1,30 @@ using System; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Visuals.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia { public static class SkiaSharpExtensions { + public static SKFilterQuality ToSKFilterQuality(this BitmapInterpolationMode interpolationMode) + { + switch (interpolationMode) + { + case BitmapInterpolationMode.LowQuality: + return SKFilterQuality.Low; + case BitmapInterpolationMode.MediumQuality: + return SKFilterQuality.Medium; + case BitmapInterpolationMode.HighQuality: + return SKFilterQuality.High; + case BitmapInterpolationMode.Default: + return SKFilterQuality.None; + default: + throw new ArgumentOutOfRangeException(nameof(interpolationMode), interpolationMode, null); + } + } + public static SKPoint ToSKPoint(this Point p) { return new SKPoint((float)p.X, (float)p.Y); diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index b8bbed24f8..534ecdd6e6 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Platform.Surfaces; using Avalonia.Direct2D1.Media; using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Media; +using Avalonia.Media.Imaging; using Avalonia.Platform; using SharpDX.DirectWrite; using GlyphRun = Avalonia.Media.GlyphRun; @@ -194,6 +195,16 @@ namespace Avalonia.Direct2D1 return new WicBitmapImpl(format, data, size, dpi, stride); } + public IBitmapImpl LoadBitmap(string fileName, BitmapDecodeOptions decodeOptions) + { + throw new NotImplementedException(); + } + + public IBitmapImpl LoadBitmap(Stream stream, BitmapDecodeOptions decodeOptions) + { + throw new NotImplementedException(); + } + public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) { var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl; From 3995b8abb97cc4b6ed386da8f1516f2014872d9b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 May 2020 17:29:41 -0300 Subject: [PATCH 05/44] use default arguments for bitmap decoder options in platform backends. --- .../Platform/IPlatformRenderInterface.cs | 10 ++--- .../Avalonia.Skia/PlatformRenderInterface.cs | 38 +++++++++---------- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 14 +------ 3 files changed, 23 insertions(+), 39 deletions(-) diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index 86662a3584..a32aeb2326 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -85,23 +85,21 @@ namespace Avalonia.Platform /// An . IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? format = null); - IBitmapImpl LoadBitmap(string fileName, BitmapDecodeOptions decodeOptions); - - IBitmapImpl LoadBitmap(Stream stream, BitmapDecodeOptions decodeOptions); - /// /// Loads a bitmap implementation from a file.. /// /// The filename of the bitmap. + /// Decode options that specify the size of the decoded bitmap. /// An . - IBitmapImpl LoadBitmap(string fileName); + IBitmapImpl LoadBitmap(string fileName, BitmapDecodeOptions? decodeOptions = null); /// /// Loads a bitmap implementation from a file.. /// /// The stream to read the bitmap from. + /// Decode options that specify the size of the decoded bitmap. /// An . - IBitmapImpl LoadBitmap(Stream stream); + IBitmapImpl LoadBitmap(Stream stream, BitmapDecodeOptions? decodeOptions = null); /// /// Loads a bitmap implementation from a pixels in memory. diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index b96a65186e..28a8b2b1ef 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -78,40 +78,36 @@ namespace Avalonia.Skia return new StreamGeometryImpl(); } - public IBitmapImpl LoadBitmap(string fileName, BitmapDecodeOptions decodeOptions) - { - using (var stream = File.OpenRead(fileName)) + /// + public unsafe IBitmapImpl LoadBitmap(Stream stream, BitmapDecodeOptions? decodeOptions = null) + { + if (decodeOptions is null) { - return LoadBitmap(stream, decodeOptions); + return new ImmutableBitmap(stream); } - } + else + { + var options = decodeOptions.Value; - public unsafe IBitmapImpl LoadBitmap(Stream stream, BitmapDecodeOptions decodeOptions) - { - var skBitmap = SKBitmap.Decode(stream); + var skBitmap = SKBitmap.Decode(stream); - skBitmap = skBitmap.Resize(new SKImageInfo(decodeOptions.DecodePixelSize.Width, decodeOptions.DecodePixelSize.Height), decodeOptions.InterpolationMode.ToSKFilterQuality()); + skBitmap = skBitmap.Resize(new SKImageInfo(options.DecodePixelSize.Width, options.DecodePixelSize.Height), options.InterpolationMode.ToSKFilterQuality()); - fixed (byte* p = skBitmap.Bytes) - { - IntPtr ptr = (IntPtr)p; + fixed (byte* p = skBitmap.Bytes) + { + IntPtr ptr = (IntPtr)p; - return LoadBitmap(PixelFormat.Bgra8888, ptr, new PixelSize(skBitmap.Width, skBitmap.Height), new Vector(96, 96), skBitmap.RowBytes); + return LoadBitmap(PixelFormat.Bgra8888, ptr, new PixelSize(skBitmap.Width, skBitmap.Height), new Vector(96, 96), skBitmap.RowBytes); + } } } /// - public IBitmapImpl LoadBitmap(Stream stream) - { - return new ImmutableBitmap(stream); - } - - /// - public IBitmapImpl LoadBitmap(string fileName) + public IBitmapImpl LoadBitmap(string fileName, BitmapDecodeOptions? decodeOptions) { using (var stream = File.OpenRead(fileName)) { - return LoadBitmap(stream); + return LoadBitmap(stream, decodeOptions); } } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 534ecdd6e6..617bae2245 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -180,12 +180,12 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateRectangleGeometry(Rect rect) => new RectangleGeometryImpl(rect); public IStreamGeometryImpl CreateStreamGeometry() => new StreamGeometryImpl(); - public IBitmapImpl LoadBitmap(string fileName) + public IBitmapImpl LoadBitmap(string fileName, BitmapDecodeOptions? options = null) { return new WicBitmapImpl(fileName); } - public IBitmapImpl LoadBitmap(Stream stream) + public IBitmapImpl LoadBitmap(Stream stream, BitmapDecodeOptions? options = null) { return new WicBitmapImpl(stream); } @@ -195,16 +195,6 @@ namespace Avalonia.Direct2D1 return new WicBitmapImpl(format, data, size, dpi, stride); } - public IBitmapImpl LoadBitmap(string fileName, BitmapDecodeOptions decodeOptions) - { - throw new NotImplementedException(); - } - - public IBitmapImpl LoadBitmap(Stream stream, BitmapDecodeOptions decodeOptions) - { - throw new NotImplementedException(); - } - public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) { var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl; From 22d263812f6fe655926ecb8b240947f8fdd5c780 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 May 2020 18:35:59 -0300 Subject: [PATCH 06/44] code tidy --- src/Skia/Avalonia.Skia/PlatformRenderInterface.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 28a8b2b1ef..baf4d066ce 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -103,7 +103,7 @@ namespace Avalonia.Skia } /// - public IBitmapImpl LoadBitmap(string fileName, BitmapDecodeOptions? decodeOptions) + public IBitmapImpl LoadBitmap(string fileName, BitmapDecodeOptions? decodeOptions = null) { using (var stream = File.OpenRead(fileName)) { From 663b91a8bf1ddef5e2e5425ba0482067328e8cc2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 9 May 2020 16:01:18 -0300 Subject: [PATCH 07/44] Implement ability to decode a bitmap at a specified width or height. --- src/Avalonia.Visuals/Media/Imaging/Bitmap.cs | 24 ++++---- .../Media/Imaging/BitmapDecodeOptions.cs | 11 ---- .../Platform/IPlatformRenderInterface.cs | 25 ++++++-- src/Skia/Avalonia.Skia/ImmutableBitmap.cs | 61 +++++++++++++++++++ .../Avalonia.Skia/PlatformRenderInterface.cs | 40 +++++------- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 15 ++++- 6 files changed, 121 insertions(+), 55 deletions(-) delete mode 100644 src/Avalonia.Visuals/Media/Imaging/BitmapDecodeOptions.cs diff --git a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs index 408a93eae7..e276b7f65d 100644 --- a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs @@ -11,6 +11,18 @@ namespace Avalonia.Media.Imaging /// public class Bitmap : IBitmap { + public static Bitmap DecodeToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); + return new Bitmap(factory.LoadBitmapToWidth(stream, width, interpolationMode)); + } + + public static Bitmap DecodeToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); + return new Bitmap(factory.LoadBitmapToHeight(stream, height, interpolationMode)); + } + /// /// Initializes a new instance of the class. /// @@ -31,18 +43,6 @@ namespace Avalonia.Media.Imaging PlatformImpl = RefCountable.Create(factory.LoadBitmap(stream)); } - public Bitmap(Stream stream, BitmapDecodeOptions decodeOptions) - { - IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); - PlatformImpl = RefCountable.Create(factory.LoadBitmap(stream, decodeOptions)); - } - - public Bitmap(string file, BitmapDecodeOptions decodeOptions) - { - IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); - PlatformImpl = RefCountable.Create(factory.LoadBitmap(file, decodeOptions)); - } - /// /// Initializes a new instance of the class. /// diff --git a/src/Avalonia.Visuals/Media/Imaging/BitmapDecodeOptions.cs b/src/Avalonia.Visuals/Media/Imaging/BitmapDecodeOptions.cs deleted file mode 100644 index ec9d823bb4..0000000000 --- a/src/Avalonia.Visuals/Media/Imaging/BitmapDecodeOptions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Visuals.Media.Imaging; - -namespace Avalonia.Media.Imaging -{ - public struct BitmapDecodeOptions - { - public PixelSize DecodePixelSize { get; set; } - - public BitmapInterpolationMode InterpolationMode { get; set; } - } -} diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index a32aeb2326..381dde4891 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Platform { @@ -88,18 +89,30 @@ namespace Avalonia.Platform /// /// Loads a bitmap implementation from a file.. /// - /// The filename of the bitmap. - /// Decode options that specify the size of the decoded bitmap. + /// The filename of the bitmap. /// An . - IBitmapImpl LoadBitmap(string fileName, BitmapDecodeOptions? decodeOptions = null); + IBitmapImpl LoadBitmap(string fileName); /// /// Loads a bitmap implementation from a file.. /// - /// The stream to read the bitmap from. - /// Decode options that specify the size of the decoded bitmap. + /// The stream to read the bitmap from. /// An . - IBitmapImpl LoadBitmap(Stream stream, BitmapDecodeOptions? decodeOptions = null); + IBitmapImpl LoadBitmap(Stream stream); + + /// + /// Loads a bitmap implementation from a file.. + /// + /// The stream to read the bitmap from. + /// An . + IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); + + /// + /// Loads a bitmap implementation from a file.. + /// + /// The stream to read the bitmap from. + /// An . + IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); /// /// Loads a bitmap implementation from a pixels in memory. diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index 9222c5ac61..887d023917 100644 --- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -1,7 +1,11 @@ using System; using System.IO; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Skia.Helpers; +using Avalonia.Visuals.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia @@ -36,6 +40,63 @@ namespace Avalonia.Skia } } + // NOTE, putting the stream before options in the parameters, causes an exception + // inside SKCodec.Create with optimized code. Probably a bug in .net compiler. + // Other option is to have the argument order as desired and use PreserveSig options. + [MethodImpl(MethodImplOptions.PreserveSig)] + public ImmutableBitmap(Stream stream, int decodeSize, bool horizontal, BitmapInterpolationMode interpolationMode) + { + // create the codec + var codec = SKCodec.Create(stream); + var info = codec.Info; + + // get the scale that is nearest to what we want (eg: jpg returned 512) + var supportedScale = codec.GetScaledDimensions(horizontal ? ((float)decodeSize / info.Width) : ((float)decodeSize / info.Height)); + + // decode the bitmap at the nearest size + var nearest = new SKImageInfo(supportedScale.Width, supportedScale.Height); + var bmp = SKBitmap.Decode(codec, nearest); + + // now scale that to the size that we want + var realScale = horizontal ? ((double)info.Height / info.Width) : ((double)info.Width / info.Height); + + SKImageInfo desired; + + + if (horizontal) + { + desired = new SKImageInfo(decodeSize, (int)(realScale * decodeSize)); + } + else + { + desired = new SKImageInfo((int)(realScale * decodeSize), decodeSize); + } + + if (bmp.Width != desired.Width || bmp.Height != desired.Height) + { + if (bmp.Height != bmp.Width) + { + + } + var scaledBmp = bmp.Resize(desired, interpolationMode.ToSKFilterQuality()); + bmp.Dispose(); + bmp = scaledBmp; + } + + _image = SKImage.FromBitmap(bmp); + bmp.Dispose(); + + if (_image == null) + { + throw new ArgumentException("Unable to load bitmap from provided data"); + } + + PixelSize = new PixelSize(_image.Width, _image.Height); + + // TODO: Skia doesn't have an API for DPI. + Dpi = new Vector(96, 96); + } + /// /// Create immutable bitmap from given pixel data copy. /// diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 6c8ca7e5b2..32fee417a4 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.IO; +using System.Security.Cryptography; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.OpenGL; using Avalonia.Platform; +using Avalonia.Visuals.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia @@ -78,37 +80,27 @@ namespace Avalonia.Skia return new StreamGeometryImpl(); } - /// - public unsafe IBitmapImpl LoadBitmap(Stream stream, BitmapDecodeOptions? decodeOptions = null) - { - if (decodeOptions is null) + public IBitmapImpl LoadBitmap(string fileName) + { + using (var stream = File.OpenRead(fileName)) { - return new ImmutableBitmap(stream); + return LoadBitmap(stream); } - else - { - var options = decodeOptions.Value; - - var skBitmap = SKBitmap.Decode(stream); - - skBitmap = skBitmap.Resize(new SKImageInfo(options.DecodePixelSize.Width, options.DecodePixelSize.Height), options.InterpolationMode.ToSKFilterQuality()); + } - fixed (byte* p = skBitmap.Bytes) - { - IntPtr ptr = (IntPtr)p; + public IBitmapImpl LoadBitmap(Stream stream) + { + return new ImmutableBitmap(stream); + } - return LoadBitmap(PixelFormat.Bgra8888, ptr, new PixelSize(skBitmap.Width, skBitmap.Height), new Vector(96, 96), skBitmap.RowBytes); - } - } + public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + return new ImmutableBitmap(stream, width, true, interpolationMode); } - /// - public IBitmapImpl LoadBitmap(string fileName, BitmapDecodeOptions? decodeOptions = null) + public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) { - using (var stream = File.OpenRead(fileName)) - { - return LoadBitmap(stream, decodeOptions); - } + return new ImmutableBitmap(stream, height, false, interpolationMode); } /// diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 617bae2245..fd6c2dffd5 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -9,6 +9,7 @@ using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; +using Avalonia.Visuals.Media.Imaging; using SharpDX.DirectWrite; using GlyphRun = Avalonia.Media.GlyphRun; using TextAlignment = Avalonia.Media.TextAlignment; @@ -180,16 +181,26 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateRectangleGeometry(Rect rect) => new RectangleGeometryImpl(rect); public IStreamGeometryImpl CreateStreamGeometry() => new StreamGeometryImpl(); - public IBitmapImpl LoadBitmap(string fileName, BitmapDecodeOptions? options = null) + public IBitmapImpl LoadBitmap(string fileName) { return new WicBitmapImpl(fileName); } - public IBitmapImpl LoadBitmap(Stream stream, BitmapDecodeOptions? options = null) + public IBitmapImpl LoadBitmap(Stream stream) { return new WicBitmapImpl(stream); } + public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + throw new NotImplementedException(); + } + + public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + throw new NotImplementedException(); + } + public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride) { return new WicBitmapImpl(format, data, size, dpi, stride); From 0889318c57ebe215f341da1fa01cdd12d39856d5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 9 May 2020 16:28:32 -0300 Subject: [PATCH 08/44] Add createscaled bitmap --- src/Avalonia.Visuals/Media/Imaging/Bitmap.cs | 6 ++++++ .../Platform/IPlatformRenderInterface.cs | 2 ++ src/Skia/Avalonia.Skia/ImmutableBitmap.cs | 14 ++++++++++++++ src/Skia/Avalonia.Skia/PlatformRenderInterface.cs | 12 ++++++++++++ .../Avalonia.Direct2D1/Direct2D1Platform.cs | 5 +++++ 5 files changed, 39 insertions(+) diff --git a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs index e276b7f65d..3d567fe458 100644 --- a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs @@ -23,6 +23,12 @@ namespace Avalonia.Media.Imaging return new Bitmap(factory.LoadBitmapToHeight(stream, height, interpolationMode)); } + public static Bitmap CreateScaledBitmap(Bitmap src, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); + return new Bitmap(factory.ResizeBitmap(src.PlatformImpl.Item, destinationSize, interpolationMode)); + } + /// /// Initializes a new instance of the class. /// diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index 381dde4891..e35d1154a1 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -114,6 +114,8 @@ namespace Avalonia.Platform /// An . IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); + IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); + /// /// Loads a bitmap implementation from a pixels in memory. /// diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index 887d023917..8323eabfbb 100644 --- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -40,6 +40,20 @@ namespace Avalonia.Skia } } + public ImmutableBitmap(ImmutableBitmap src, PixelSize destinationSize, BitmapInterpolationMode interpolationMode) + { + SKImageInfo info = new SKImageInfo(destinationSize.Width, destinationSize.Height, SKColorType.Bgra8888); + SKImage output = SKImage.Create(info); + src._image.ScalePixels(output.PeekPixels(), interpolationMode.ToSKFilterQuality()); + + _image = output; + + PixelSize = new PixelSize(_image.Width, _image.Height); + + // TODO: Skia doesn't have an API for DPI. + Dpi = new Vector(96, 96); + } + // NOTE, putting the stream before options in the parameters, causes an exception // inside SKCodec.Create with optimized code. Probably a bug in .net compiler. // Other option is to have the argument order as desired and use PreserveSig options. diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 32fee417a4..f744b2f00a 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -109,6 +109,18 @@ namespace Avalonia.Skia return new ImmutableBitmap(size, dpi, stride, format, data); } + public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + if (bitmapImpl is ImmutableBitmap ibmp) + { + return new ImmutableBitmap(ibmp, destinationSize, interpolationMode); + } + else + { + throw new Exception("Invalid source bitmap type."); + } + } + /// public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) { diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index fd6c2dffd5..ef5b3255c1 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -201,6 +201,11 @@ namespace Avalonia.Direct2D1 throw new NotImplementedException(); } + public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + throw new NotImplementedException(); + } + public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride) { return new WicBitmapImpl(format, data, size, dpi, stride); From ac65b4e71762c7d072a83d4668f36225d911ac84 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 9 May 2020 16:40:33 -0300 Subject: [PATCH 09/44] Add documentation. --- src/Avalonia.Visuals/Media/Imaging/Bitmap.cs | 27 +++++++++++++++++-- .../Platform/IPlatformRenderInterface.cs | 14 ++++++---- src/Skia/Avalonia.Skia/ImmutableBitmap.cs | 2 +- .../Avalonia.Skia/PlatformRenderInterface.cs | 15 +++++++---- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 6 +++++ 5 files changed, 51 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs index 3d567fe458..af69e75571 100644 --- a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs @@ -11,18 +11,41 @@ namespace Avalonia.Media.Imaging /// public class Bitmap : IBitmap { + /// + /// Loads a Bitmap from a stream and decodes at the desired width. Aspect ratio is maintained. + /// This is more efficient than loading and then resizing. + /// + /// The stream to read the bitmap from. This can be any supported image format. + /// The desired width of the resulting bitmap. + /// The to use should any scaling be required. + /// An instance of the class. public static Bitmap DecodeToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) { IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); return new Bitmap(factory.LoadBitmapToWidth(stream, width, interpolationMode)); } + /// + /// Loads a Bitmap from a stream and decodes at the desired height. Aspect ratio is maintained. + /// This is more efficient than loading and then resizing. + /// + /// The stream to read the bitmap from. This can be any supported image format. + /// The desired height of the resulting bitmap. + /// The to use should any scaling be required. + /// An instance of the class. public static Bitmap DecodeToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) { IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); return new Bitmap(factory.LoadBitmapToHeight(stream, height, interpolationMode)); } + /// + /// Creates a Bitmap from another Bitmap scaled to a specified size. + /// + /// The source bitmap. + /// The destination size. + /// The to use should any scaling be required. + /// An instance of the class. public static Bitmap CreateScaledBitmap(Bitmap src, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) { IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); @@ -57,7 +80,7 @@ namespace Avalonia.Media.Imaging { PlatformImpl = impl.Clone(); } - + /// /// Initializes a new instance of the class. /// @@ -66,7 +89,7 @@ namespace Avalonia.Media.Imaging { PlatformImpl = RefCountable.Create(impl); } - + /// public virtual void Dispose() { diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index e35d1154a1..c0ef65943a 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -91,7 +91,7 @@ namespace Avalonia.Platform /// /// The filename of the bitmap. /// An . - IBitmapImpl LoadBitmap(string fileName); + IBitmapImpl LoadBitmap(string fileName); /// /// Loads a bitmap implementation from a file.. @@ -101,16 +101,20 @@ namespace Avalonia.Platform IBitmapImpl LoadBitmap(Stream stream); /// - /// Loads a bitmap implementation from a file.. + /// Loads a bitmap implementation from a stream to a specified width maintaining aspect ratio. /// - /// The stream to read the bitmap from. + /// The stream to read the bitmap from. + /// The desired width of the resulting bitmap. + /// The to use should resizing be required. /// An . IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); /// - /// Loads a bitmap implementation from a file.. + /// Loads a bitmap implementation from a stream to a specified height maintaining aspect ratio. /// - /// The stream to read the bitmap from. + /// The stream to read the bitmap from. + /// The desired height of the resulting bitmap. + /// The to use should resizing be required. /// An . IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index 8323eabfbb..4fc02b28e1 100644 --- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -41,7 +41,7 @@ namespace Avalonia.Skia } public ImmutableBitmap(ImmutableBitmap src, PixelSize destinationSize, BitmapInterpolationMode interpolationMode) - { + { SKImageInfo info = new SKImageInfo(destinationSize.Width, destinationSize.Height, SKColorType.Bgra8888); SKImage output = SKImage.Create(info); src._image.ScalePixels(output.PeekPixels(), interpolationMode.ToSKFilterQuality()); diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index f744b2f00a..869ca6969b 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -80,6 +80,7 @@ namespace Avalonia.Skia return new StreamGeometryImpl(); } + /// public IBitmapImpl LoadBitmap(string fileName) { using (var stream = File.OpenRead(fileName)) @@ -88,27 +89,31 @@ namespace Avalonia.Skia } } + /// public IBitmapImpl LoadBitmap(Stream stream) { return new ImmutableBitmap(stream); } + /// + public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride) + { + return new ImmutableBitmap(size, dpi, stride, format, data); + } + + /// public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) { return new ImmutableBitmap(stream, width, true, interpolationMode); } + /// public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) { return new ImmutableBitmap(stream, height, false, interpolationMode); } /// - public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride) - { - return new ImmutableBitmap(size, dpi, stride, format, data); - } - public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) { if (bitmapImpl is ImmutableBitmap ibmp) diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index ef5b3255c1..fa96a5eebf 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -181,31 +181,37 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateRectangleGeometry(Rect rect) => new RectangleGeometryImpl(rect); public IStreamGeometryImpl CreateStreamGeometry() => new StreamGeometryImpl(); + /// public IBitmapImpl LoadBitmap(string fileName) { return new WicBitmapImpl(fileName); } + /// public IBitmapImpl LoadBitmap(Stream stream) { return new WicBitmapImpl(stream); } + /// public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) { throw new NotImplementedException(); } + /// public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) { throw new NotImplementedException(); } + /// public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) { throw new NotImplementedException(); } + /// public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride) { return new WicBitmapImpl(format, data, size, dpi, stride); From 5d8a03d2aa9722e7c7d3420b3d0597c30721d67d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 9 May 2020 16:51:02 -0300 Subject: [PATCH 10/44] fix tests --- .../MockPlatformRenderInterface.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 23b6a00cc8..c5d9699c01 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Visuals.Media.Imaging; using Moq; namespace Avalonia.UnitTests @@ -69,6 +70,21 @@ namespace Avalonia.UnitTests return Mock.Of(); } + public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + return Mock.Of(); + } + + public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + return Mock.Of(); + } + + public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + return Mock.Of(); + } + public IBitmapImpl LoadBitmap( PixelFormat format, IntPtr data, From ab9556257c90a26582b7960a2000e4baaf3bd34f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 9 May 2020 17:48:49 -0300 Subject: [PATCH 11/44] fix test interfaces --- .../Avalonia.Benchmarks/NullRenderingPlatform.cs | 16 ++++++++++++++++ .../VisualTree/MockRenderInterface.cs | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 1f983069c2..62491f62c6 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -4,6 +4,7 @@ using System.IO; using Avalonia.Media; using Avalonia.Platform; using Avalonia.UnitTests; +using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Benchmarks { @@ -65,6 +66,21 @@ namespace Avalonia.Benchmarks throw new NotImplementedException(); } + public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + throw new NotImplementedException(); + } + + public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + throw new NotImplementedException(); + } + + public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + throw new NotImplementedException(); + } + public IFontManagerImpl CreateFontManager() { return new MockFontManagerImpl(); diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 28304b674b..710556ec97 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -4,6 +4,7 @@ using System.IO; using Avalonia.Media; using Avalonia.Platform; using Avalonia.UnitTests; +using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Visuals.UnitTests.VisualTree { @@ -81,6 +82,21 @@ namespace Avalonia.Visuals.UnitTests.VisualTree throw new NotImplementedException(); } + public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + throw new NotImplementedException(); + } + + public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + throw new NotImplementedException(); + } + + public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + throw new NotImplementedException(); + } + class MockStreamGeometry : IStreamGeometryImpl { private MockStreamGeometryContext _impl = new MockStreamGeometryContext(); From 4d7c89b00420041a9f5ef01b5b27e2b4894f061e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 11 May 2020 09:33:07 -0300 Subject: [PATCH 12/44] make CreateScaledBitmap an instance method --- src/Avalonia.Visuals/Media/Imaging/Bitmap.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs index af69e75571..86e2700c04 100644 --- a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs @@ -40,16 +40,15 @@ namespace Avalonia.Media.Imaging } /// - /// Creates a Bitmap from another Bitmap scaled to a specified size. - /// - /// The source bitmap. + /// Creates a Bitmap scaled to a specified size from the current bitmap. + /// /// The destination size. /// The to use should any scaling be required. /// An instance of the class. - public static Bitmap CreateScaledBitmap(Bitmap src, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + public Bitmap CreateScaledBitmap(PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) { IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); - return new Bitmap(factory.ResizeBitmap(src.PlatformImpl.Item, destinationSize, interpolationMode)); + return new Bitmap(factory.ResizeBitmap(PlatformImpl.Item, destinationSize, interpolationMode)); } /// From 6bb873d5bb6c651981830c7dd6eff933a623d9b2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 11 May 2020 12:48:59 -0300 Subject: [PATCH 13/44] implement bitmap resize apis for direct2d --- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 6 +- .../Media/Imaging/WicBitmapImpl.cs | 76 ++++++++++++++++++- 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index fa96a5eebf..dc52d4eefb 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -196,19 +196,19 @@ namespace Avalonia.Direct2D1 /// public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) { - throw new NotImplementedException(); + return new WicBitmapImpl(stream, width, true, interpolationMode); } /// public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) { - throw new NotImplementedException(); + return new WicBitmapImpl(stream, height, false, interpolationMode); } /// public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) { - throw new NotImplementedException(); + return (bitmapImpl as WicBitmapImpl).CreateScaledBitmap(destinationSize, interpolationMode); } /// diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs index f5159b1f84..c07f8f98e6 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Security.Cryptography; using Avalonia.Win32.Interop; using SharpDX.WIC; using APixelFormat = Avalonia.Platform.PixelFormat; @@ -14,6 +15,26 @@ namespace Avalonia.Direct2D1.Media { private BitmapDecoder _decoder; + private static BitmapInterpolationMode ConvertInterpolationMode(Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode interpolationMode) + { + switch (interpolationMode) + { + case Visuals.Media.Imaging.BitmapInterpolationMode.Default: + return BitmapInterpolationMode.Fant; + + case Visuals.Media.Imaging.BitmapInterpolationMode.LowQuality: + return BitmapInterpolationMode.NearestNeighbor; + + case Visuals.Media.Imaging.BitmapInterpolationMode.MediumQuality: + return BitmapInterpolationMode.Fant; + + default: + case Visuals.Media.Imaging.BitmapInterpolationMode.HighQuality: + return BitmapInterpolationMode.HighQualityCubic; + + } + } + /// /// Initializes a new instance of the class. /// @@ -27,6 +48,12 @@ namespace Avalonia.Direct2D1.Media } } + private WicBitmapImpl(Bitmap bmp) + { + WicImpl = bmp; + Dpi = new Vector(96, 96); + } + /// /// Initializes a new instance of the class. /// @@ -60,7 +87,7 @@ namespace Avalonia.Direct2D1.Media size.Height, pixelFormat.Value.ToWic(), BitmapCreateCacheOption.CacheOnLoad); - WicImpl.SetResolution(dpi.X, dpi.Y); + Dpi = dpi; } @@ -84,6 +111,43 @@ namespace Avalonia.Direct2D1.Media } } + public WicBitmapImpl(Stream stream, int decodeSize, bool horizontal, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode interpolationMode) + { + _decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, stream, DecodeOptions.CacheOnLoad); + + var bmp = new Bitmap(Direct2D1Platform.ImagingFactory, _decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnLoad); + + // now scale that to the size that we want + var realScale = horizontal ? ((double)bmp.Size.Height / bmp.Size.Width) : ((double)bmp.Size.Width / bmp.Size.Height); + + PixelSize desired; + + if (horizontal) + { + desired = new PixelSize(decodeSize, (int)(realScale * decodeSize)); + } + else + { + desired = new PixelSize((int)(realScale * decodeSize), decodeSize); + } + + if (bmp.Size.Width != desired.Width || bmp.Size.Height != desired.Height) + { + using (var scaler = new BitmapScaler(Direct2D1Platform.ImagingFactory)) + { + scaler.Initialize(bmp, desired.Width, desired.Height, ConvertInterpolationMode(interpolationMode)); + + var image = new Bitmap(Direct2D1Platform.ImagingFactory, scaler, BitmapCreateCacheOption.CacheOnDemand); + bmp.Dispose(); + bmp = image; + } + } + + WicImpl = bmp; + + Dpi = new Vector(96, 96); + } + public override Vector Dpi { get; } public override PixelSize PixelSize => WicImpl.Size.ToAvalonia(); @@ -113,6 +177,16 @@ namespace Avalonia.Direct2D1.Media return new OptionalDispose(D2DBitmap.FromWicBitmap(renderTarget, converter), true); } + public unsafe WicBitmapImpl CreateScaledBitmap(PixelSize size, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode interpolationMode) + { + using (var scaler = new BitmapScaler(Direct2D1Platform.ImagingFactory)) + { + scaler.Initialize(WicImpl, size.Width, size.Height, ConvertInterpolationMode(interpolationMode)); + + return new WicBitmapImpl(new Bitmap(Direct2D1Platform.ImagingFactory, scaler, BitmapCreateCacheOption.CacheOnDemand)); + } + } + public override void Save(Stream stream) { using (var encoder = new PngBitmapEncoder(Direct2D1Platform.ImagingFactory, stream)) From 238ba26daa280e2d489cbcf0f1e1a2374cd25fa5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 11 May 2020 13:29:51 -0300 Subject: [PATCH 14/44] use decoder frame directly to load at scaled size. --- .../Media/Imaging/WicBitmapImpl.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs index c07f8f98e6..d6887ef9ab 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs @@ -115,10 +115,10 @@ namespace Avalonia.Direct2D1.Media { _decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, stream, DecodeOptions.CacheOnLoad); - var bmp = new Bitmap(Direct2D1Platform.ImagingFactory, _decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnLoad); + var frame = _decoder.GetFrame(0); // now scale that to the size that we want - var realScale = horizontal ? ((double)bmp.Size.Height / bmp.Size.Width) : ((double)bmp.Size.Width / bmp.Size.Height); + var realScale = horizontal ? ((double)frame.Size.Height / frame.Size.Width) : ((double)frame.Size.Width / frame.Size.Height); PixelSize desired; @@ -131,19 +131,19 @@ namespace Avalonia.Direct2D1.Media desired = new PixelSize((int)(realScale * decodeSize), decodeSize); } - if (bmp.Size.Width != desired.Width || bmp.Size.Height != desired.Height) + if (frame.Size.Width != desired.Width || frame.Size.Height != desired.Height) { using (var scaler = new BitmapScaler(Direct2D1Platform.ImagingFactory)) { - scaler.Initialize(bmp, desired.Width, desired.Height, ConvertInterpolationMode(interpolationMode)); + scaler.Initialize(frame, desired.Width, desired.Height, ConvertInterpolationMode(interpolationMode)); - var image = new Bitmap(Direct2D1Platform.ImagingFactory, scaler, BitmapCreateCacheOption.CacheOnDemand); - bmp.Dispose(); - bmp = image; + WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, scaler, BitmapCreateCacheOption.CacheOnLoad); } } - - WicImpl = bmp; + else + { + WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, frame, BitmapCreateCacheOption.CacheOnLoad); + } Dpi = new Vector(96, 96); } From 4dbaaf4b1f5aa7b9e8ff63617f27fdb28f84dead Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 11 May 2020 17:37:32 -0300 Subject: [PATCH 15/44] disable optimizations for method. --- src/Skia/Avalonia.Skia/ImmutableBitmap.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index 4fc02b28e1..9410895f80 100644 --- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -54,10 +54,8 @@ namespace Avalonia.Skia Dpi = new Vector(96, 96); } - // NOTE, putting the stream before options in the parameters, causes an exception - // inside SKCodec.Create with optimized code. Probably a bug in .net compiler. - // Other option is to have the argument order as desired and use PreserveSig options. - [MethodImpl(MethodImplOptions.PreserveSig)] + //NOTE: SKCodec.Create randomly crashes when optimizations are enabled. + [MethodImpl(MethodImplOptions.NoOptimization)] public ImmutableBitmap(Stream stream, int decodeSize, bool horizontal, BitmapInterpolationMode interpolationMode) { // create the codec From b08c60bd1466b31cbe9b2b4a5ecf81b9865e53c9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 11 May 2020 18:09:46 -0300 Subject: [PATCH 16/44] wrap stream in skmanagedstream to prevent collection of underlying stream, dispose codec. --- src/Skia/Avalonia.Skia/ImmutableBitmap.cs | 74 +++++++++++------------ 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index 9410895f80..e84c7e34de 100644 --- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -54,59 +54,59 @@ namespace Avalonia.Skia Dpi = new Vector(96, 96); } - //NOTE: SKCodec.Create randomly crashes when optimizations are enabled. - [MethodImpl(MethodImplOptions.NoOptimization)] public ImmutableBitmap(Stream stream, int decodeSize, bool horizontal, BitmapInterpolationMode interpolationMode) { - // create the codec - var codec = SKCodec.Create(stream); - var info = codec.Info; + using (var skStream = new SKManagedStream(stream)) + using (var codec = SKCodec.Create(skStream)) + { + var info = codec.Info; - // get the scale that is nearest to what we want (eg: jpg returned 512) - var supportedScale = codec.GetScaledDimensions(horizontal ? ((float)decodeSize / info.Width) : ((float)decodeSize / info.Height)); + // get the scale that is nearest to what we want (eg: jpg returned 512) + var supportedScale = codec.GetScaledDimensions(horizontal ? ((float)decodeSize / info.Width) : ((float)decodeSize / info.Height)); - // decode the bitmap at the nearest size - var nearest = new SKImageInfo(supportedScale.Width, supportedScale.Height); - var bmp = SKBitmap.Decode(codec, nearest); + // decode the bitmap at the nearest size + var nearest = new SKImageInfo(supportedScale.Width, supportedScale.Height); + var bmp = SKBitmap.Decode(codec, nearest); - // now scale that to the size that we want - var realScale = horizontal ? ((double)info.Height / info.Width) : ((double)info.Width / info.Height); + // now scale that to the size that we want + var realScale = horizontal ? ((double)info.Height / info.Width) : ((double)info.Width / info.Height); - SKImageInfo desired; + SKImageInfo desired; - if (horizontal) - { - desired = new SKImageInfo(decodeSize, (int)(realScale * decodeSize)); - } - else - { - desired = new SKImageInfo((int)(realScale * decodeSize), decodeSize); - } + if (horizontal) + { + desired = new SKImageInfo(decodeSize, (int)(realScale * decodeSize)); + } + else + { + desired = new SKImageInfo((int)(realScale * decodeSize), decodeSize); + } - if (bmp.Width != desired.Width || bmp.Height != desired.Height) - { - if (bmp.Height != bmp.Width) + if (bmp.Width != desired.Width || bmp.Height != desired.Height) { + if (bmp.Height != bmp.Width) + { + } + var scaledBmp = bmp.Resize(desired, interpolationMode.ToSKFilterQuality()); + bmp.Dispose(); + bmp = scaledBmp; } - var scaledBmp = bmp.Resize(desired, interpolationMode.ToSKFilterQuality()); - bmp.Dispose(); - bmp = scaledBmp; - } - _image = SKImage.FromBitmap(bmp); - bmp.Dispose(); + _image = SKImage.FromBitmap(bmp); + bmp.Dispose(); - if (_image == null) - { - throw new ArgumentException("Unable to load bitmap from provided data"); - } + if (_image == null) + { + throw new ArgumentException("Unable to load bitmap from provided data"); + } - PixelSize = new PixelSize(_image.Width, _image.Height); + PixelSize = new PixelSize(_image.Width, _image.Height); - // TODO: Skia doesn't have an API for DPI. - Dpi = new Vector(96, 96); + // TODO: Skia doesn't have an API for DPI. + Dpi = new Vector(96, 96); + } } /// From a856b210fbb7fa84729ac2a162736799b7c0c30a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 13 May 2020 12:47:49 -0300 Subject: [PATCH 17/44] dont implement CreateScaleBitmap on Direct2d. --- src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs | 3 ++- .../Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs | 10 ---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 942a98586e..c4c0541d53 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -208,7 +208,8 @@ namespace Avalonia.Direct2D1 /// public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) { - return (bitmapImpl as WicBitmapImpl).CreateScaledBitmap(destinationSize, interpolationMode); + // https://github.com/sharpdx/SharpDX/issues/959 blocks implementation. + throw new NotImplementedException(); } /// diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs index d6887ef9ab..743abddd1e 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs @@ -177,16 +177,6 @@ namespace Avalonia.Direct2D1.Media return new OptionalDispose(D2DBitmap.FromWicBitmap(renderTarget, converter), true); } - public unsafe WicBitmapImpl CreateScaledBitmap(PixelSize size, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode interpolationMode) - { - using (var scaler = new BitmapScaler(Direct2D1Platform.ImagingFactory)) - { - scaler.Initialize(WicImpl, size.Width, size.Height, ConvertInterpolationMode(interpolationMode)); - - return new WicBitmapImpl(new Bitmap(Direct2D1Platform.ImagingFactory, scaler, BitmapCreateCacheOption.CacheOnDemand)); - } - } - public override void Save(Stream stream) { using (var encoder = new PngBitmapEncoder(Direct2D1Platform.ImagingFactory, stream)) From d284e8b253078311fa38103d2823aa589c9d5098 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 14 May 2020 15:12:57 +0200 Subject: [PATCH 18/44] Fix NextDrawAs. --- .../Rendering/SceneGraph/DeferredDrawingContextImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index b8658a7a26..6ad71ac111 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -364,7 +364,7 @@ namespace Avalonia.Rendering.SceneGraph public int DrawOperationIndex { get; } } - private void Add(IDrawOperation node) + private void Add(T node) where T : class, IDrawOperation { using (var refCounted = RefCountable.Create(node)) { From 6e7d2bec62e5f0169c89e2cc13aba4fbdecfe4d7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 11:21:19 -0300 Subject: [PATCH 19/44] add new IWindowImpl members, remove ShowDialog. --- src/Avalonia.Controls/Platform/IWindowImpl.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 5fa0ec57b5..cf31d30332 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -26,9 +26,21 @@ namespace Avalonia.Platform void SetTitle(string title); /// - /// Shows the window as a dialog. + /// Sets the parent of the window. /// - void ShowDialog(IWindowImpl parent); + /// The parent . + void SetParent(IWindowImpl parent); + + /// + /// Disables the window for example when a modal dialog is open. + /// + /// true if the window is enabled, or false if it is disabled. + void SetEnabled(bool enable); + + /// + /// Called when a disabled window received input. Can be used to activate child windows. + /// + Action GotInputWhenDisabled { get; set; } /// /// Enables or disables system window decorations (title bar, buttons, etc) From e434d68f2e68422318b0f8d0024f73b089dd071f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 11:32:44 -0300 Subject: [PATCH 20/44] implement showdialog using new apis. --- src/Avalonia.Controls/Window.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 7dacf4b2af..7fa21669f7 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reactive.Linq; @@ -519,7 +520,9 @@ namespace Avalonia.Controls using (BeginAutoSizing()) { - PlatformImpl?.ShowDialog(owner.PlatformImpl); + PlatformImpl.SetParent(owner.PlatformImpl); + owner.PlatformImpl.SetEnabled(false); + PlatformImpl?.Show(); Renderer?.Start(); From a8418189d2bc6dc268e7fccb037a840911c3abc7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 11:34:03 -0300 Subject: [PATCH 21/44] win32 implementation uses new apis for showdialog. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 608563cb67..d57884c29e 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -354,19 +354,20 @@ namespace Avalonia.Win32 public virtual void Show() { - SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, IntPtr.Zero); + SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, _parent != null ? _parent._hwnd : IntPtr.Zero); ShowWindow(_showWindowState); } - public void ShowDialog(IWindowImpl parent) + public Action GotInputWhenDisabled { get; set; } + + public void SetParent(IWindowImpl parent) { _parent = (WindowImpl)parent; _parent._disabledBy.Add(this); - _parent.UpdateEnabled(); - SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, ((WindowImpl)parent)._hwnd); - ShowWindow(_showWindowState); } + public void SetEnabled(bool enable) => EnableWindow(_hwnd, enable); + public void BeginMoveDrag(PointerPressedEventArgs e) { _mouseDevice.Capture(null); @@ -723,7 +724,7 @@ namespace Avalonia.Win32 private void UpdateEnabled() { - EnableWindow(_hwnd, _disabledBy.Count == 0); + SetEnabled(_disabledBy.Count == 0); } private void UpdateWindowProperties(WindowProperties newProperties, bool forceChanges = false) From 4fe8e41646e5a483780a76726ee8a590975c2f7f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 11:36:58 -0300 Subject: [PATCH 22/44] add x11 implementation. --- src/Avalonia.X11/X11Window.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 1b3d1a7dda..eb1759aadd 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -44,6 +44,8 @@ namespace Avalonia.X11 private HashSet _transientChildren = new HashSet(); private X11Window _transientParent; private double? _scalingOverride; + private bool _disabled; + public object SyncRoot { get; } = new object(); class InputEventContainer @@ -773,6 +775,11 @@ namespace Avalonia.X11 bool ActivateTransientChildIfNeeded() { + if(_disabled) + { + return false; + } + if (_transientChildren.Count == 0) return false; var child = _transientChildren.First(); @@ -1035,12 +1042,18 @@ namespace Avalonia.X11 ChangeWMAtoms(value, _x11.Atoms._NET_WM_STATE_ABOVE); } - public void ShowDialog(IWindowImpl parent) + public void SetParent(IWindowImpl parent) { SetTransientParent((X11Window)parent); - ShowCore(); } + public void SetEnabled(bool enable) + { + _disabled = !enable; + } + + public Action GotInputWhenDisabled { get; set; } + public void SetIcon(IWindowIconImpl icon) { if (icon != null) From cfdd21e43285d684b359a694971bac484bd662fd Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 11:37:24 -0300 Subject: [PATCH 23/44] add stubs for osx implementation. --- src/Avalonia.Native/WindowImpl.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 03a7b4fceb..25cccb6140 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -56,11 +56,6 @@ namespace Avalonia.Native public IAvnWindow Native => _native; - public void ShowDialog(IWindowImpl window) - { - _native.ShowDialog(((WindowImpl)window).Native); - } - public void CanResize(bool value) { _native.CanResize = value; @@ -116,5 +111,15 @@ namespace Avalonia.Native public override IPopupImpl CreatePopup() => _opts.OverlayPopups ? null : new PopupImpl(_factory, _opts, _glFeature, this); + + public Action GotInputWhenDisabled { get; set; } + + public void SetParent(IWindowImpl parent) + { + } + + public void SetEnabled(bool enable) + { + } } } From e1c1775e71cb967aeb48488ecb16296b1af83466 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 11:37:31 -0300 Subject: [PATCH 24/44] stubs --- .../Remote/PreviewerWindowImpl.cs | 9 +++++++++ src/Avalonia.DesignerSupport/Remote/Stubs.cs | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 7480b3519c..844489ef97 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -83,6 +83,7 @@ namespace Avalonia.DesignerSupport.Remote } public IScreenImpl Screen { get; } = new ScreenStub(); + public Action GotInputWhenDisabled { get; set; } public void Activate() { @@ -115,5 +116,13 @@ namespace Avalonia.DesignerSupport.Remote public void SetTopmost(bool value) { } + + public void SetParent(IWindowImpl parent) + { + } + + public void SetEnabled(bool enable) + { + } } } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 82950ce53b..484cf3bc97 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -130,7 +130,17 @@ namespace Avalonia.DesignerSupport.Remote { } + public void SetParent(IWindowImpl parent) + { + } + + public void SetEnabled(bool enable) + { + } + public IPopupPositioner PopupPositioner { get; } + + public Action GotInputWhenDisabled { get; set; } } class ClipboardStub : IClipboard From 0116f7ad9616dcff55ccdb987c93b9ee62827a0a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 11:58:09 -0300 Subject: [PATCH 25/44] remove disabledBy array on win32. --- src/Avalonia.Controls/Window.cs | 20 +++++++++++++++++++- src/Windows/Avalonia.Win32/WindowImpl.cs | 22 +++++----------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 7fa21669f7..ea4cdb395f 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -69,6 +69,9 @@ namespace Avalonia.Controls /// public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot { + private List _children = new List(); + private Window _owner; + /// /// Defines the property. /// @@ -366,6 +369,12 @@ namespace Avalonia.Controls { if (close) { + if(_owner != null) + { + _owner._children.Remove(this); + _owner = null; + } + PlatformImpl?.Dispose(); } } @@ -408,6 +417,14 @@ namespace Avalonia.Controls using (BeginAutoSizing()) { Renderer?.Stop(); + + if (_owner != null) + { + _owner._children.Remove(this); + // update enabled state of parent. + _owner = null; + } + PlatformImpl?.Hide(); } @@ -521,7 +538,8 @@ namespace Avalonia.Controls using (BeginAutoSizing()) { PlatformImpl.SetParent(owner.PlatformImpl); - owner.PlatformImpl.SetEnabled(false); + owner._children.Add(this); + owner.PlatformImpl.SetEnabled(false); PlatformImpl?.Show(); Renderer?.Start(); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index d57884c29e..192fa2ec0e 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -44,8 +44,7 @@ namespace Avalonia.Win32 private readonly ManagedWindowResizeDragHelper _managedDrag; #endif - private const WindowStyles WindowStateMask = (WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE); - private readonly List _disabledBy; + private const WindowStyles WindowStateMask = (WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE); private readonly TouchDevice _touchDevice; private readonly MouseDevice _mouseDevice; private readonly ManagedDeferredRendererLock _rendererLock; @@ -70,7 +69,6 @@ namespace Avalonia.Win32 public WindowImpl() { - _disabledBy = new List(); _touchDevice = new TouchDevice(); _mouseDevice = new WindowsMouseDevice(); @@ -342,13 +340,6 @@ namespace Avalonia.Win32 public void Hide() { - if (_parent != null) - { - _parent._disabledBy.Remove(this); - _parent.UpdateEnabled(); - _parent = null; - } - UnmanagedMethods.ShowWindow(_hwnd, ShowWindowCommand.Hide); } @@ -363,7 +354,7 @@ namespace Avalonia.Win32 public void SetParent(IWindowImpl parent) { _parent = (WindowImpl)parent; - _parent._disabledBy.Add(this); + SetOwnerHandle(_parent._hwnd); } public void SetEnabled(bool enable) => EnableWindow(_hwnd, enable); @@ -665,7 +656,9 @@ namespace Avalonia.Win32 SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); } } - } + } + + private void SetOwnerHandle(IntPtr handle) => SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, handle); private WindowStyles GetWindowStateStyles () { @@ -722,11 +715,6 @@ namespace Avalonia.Win32 } } - private void UpdateEnabled() - { - SetEnabled(_disabledBy.Count == 0); - } - private void UpdateWindowProperties(WindowProperties newProperties, bool forceChanges = false) { var oldProperties = _windowProperties; From 5dffe72af887f89526780c956f00f4267eb6a70c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 12:14:38 -0300 Subject: [PATCH 26/44] Closing event re-enabled parent. --- src/Avalonia.Controls/Window.cs | 65 ++++++++++++++++--- .../Avalonia.Win32/WindowImpl.WndProc.cs | 6 -- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index ea4cdb395f..cd033ac8ec 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -135,7 +135,7 @@ namespace Avalonia.Controls /// public static readonly RoutedEvent WindowClosedEvent = RoutedEvent.Register("WindowClosed", RoutingStrategies.Direct); - + /// /// Routed event that can be used for global tracking of opening windows /// @@ -187,6 +187,7 @@ namespace Avalonia.Controls : base(impl) { impl.Closing = HandleClosing; + impl.GotInputWhenDisabled = OnGotInputWhenDisabled; impl.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size); this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x)); @@ -306,7 +307,7 @@ namespace Avalonia.Controls PlatformImpl?.Move(value); } } - + /// /// Starts moving a window with left button being held. Should be called from left mouse button press event handler /// @@ -327,7 +328,7 @@ namespace Avalonia.Controls /// /// Fired before a window is closed. /// - public event EventHandler Closing; + public event EventHandler Closing; /// /// Closes the window. @@ -369,9 +370,9 @@ namespace Avalonia.Controls { if (close) { - if(_owner != null) + if (_owner != null) { - _owner._children.Remove(this); + _owner.RemoveChild(this); _owner = null; } @@ -387,6 +388,16 @@ namespace Avalonia.Controls { var args = new CancelEventArgs(); OnClosing(args); + + if(!args.Cancel) + { + if(_owner != null) + { + _owner.RemoveChild(this); + _owner = null; + } + } + return args.Cancel; } @@ -420,8 +431,7 @@ namespace Avalonia.Controls if (_owner != null) { - _owner._children.Remove(this); - // update enabled state of parent. + _owner.RemoveChild(this); _owner = null; } @@ -538,8 +548,8 @@ namespace Avalonia.Controls using (BeginAutoSizing()) { PlatformImpl.SetParent(owner.PlatformImpl); - owner._children.Add(this); - owner.PlatformImpl.SetEnabled(false); + _owner = owner; + _owner.AddChild(this); PlatformImpl?.Show(); Renderer?.Start(); @@ -562,6 +572,37 @@ namespace Avalonia.Controls return result.Task; } + private void UpdateEnabled() + { + PlatformImpl.SetEnabled(_children.Count == 0); + } + + private void AddChild(Window window) + { + _children.Add(window); + UpdateEnabled(); + } + + private void RemoveChild(Window window) + { + _children.Remove(window); + UpdateEnabled(); + } + + private void OnGotInputWhenDisabled() + { + var firstChild = _children.FirstOrDefault(); + + if (firstChild != null) + { + firstChild.OnGotInputWhenDisabled(); + } + else + { + Activate(); + } + } + private void SetWindowStartupLocation(IWindowBaseImpl owner = null) { var scaling = owner?.Scaling ?? PlatformImpl?.Scaling ?? 1; @@ -652,6 +693,12 @@ namespace Avalonia.Controls RaiseEvent(new RoutedEventArgs(WindowClosedEvent)); base.HandleClosed(); + + if (_owner != null) + { + _owner.RemoveChild(this); + _owner = null; + } } /// diff --git a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs index 3ea8c1e48f..138553b962 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs @@ -77,12 +77,6 @@ namespace Avalonia.Win32 s_instances.Remove(this); Closed?.Invoke(); - if (_parent != null) - { - _parent._disabledBy.Remove(this); - _parent.UpdateEnabled(); - } - _mouseDevice.Dispose(); _touchDevice?.Dispose(); //Free other resources From c8e27b597c6f0d781b33380e8c9d20fc0ab17e51 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 12:17:59 -0300 Subject: [PATCH 27/44] use owner property. --- src/Avalonia.Controls/Window.cs | 35 ++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index cd033ac8ec..9e0dae2e1f 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -69,8 +69,7 @@ namespace Avalonia.Controls /// public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot { - private List _children = new List(); - private Window _owner; + private List _children = new List(); /// /// Defines the property. @@ -370,12 +369,13 @@ namespace Avalonia.Controls { if (close) { - if (_owner != null) + if (Owner is Window owner) { - _owner.RemoveChild(this); - _owner = null; + owner.RemoveChild(this); } + Owner = null; + PlatformImpl?.Dispose(); } } @@ -391,11 +391,12 @@ namespace Avalonia.Controls if(!args.Cancel) { - if(_owner != null) + if (Owner is Window owner) { - _owner.RemoveChild(this); - _owner = null; + owner.RemoveChild(this); } + + Owner = null; } return args.Cancel; @@ -429,12 +430,13 @@ namespace Avalonia.Controls { Renderer?.Stop(); - if (_owner != null) + if (Owner is Window owner) { - _owner.RemoveChild(this); - _owner = null; + owner.RemoveChild(this); } + Owner = null; + PlatformImpl?.Hide(); } @@ -548,8 +550,8 @@ namespace Avalonia.Controls using (BeginAutoSizing()) { PlatformImpl.SetParent(owner.PlatformImpl); - _owner = owner; - _owner.AddChild(this); + Owner = owner; + owner.AddChild(this); PlatformImpl?.Show(); Renderer?.Start(); @@ -694,11 +696,12 @@ namespace Avalonia.Controls base.HandleClosed(); - if (_owner != null) + if (Owner is Window owner) { - _owner.RemoveChild(this); - _owner = null; + owner.RemoveChild(this); } + + Owner = null; } /// From 9b2ff91ea66092d9f89ec1550cb1715093da06ed Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 12:27:20 -0300 Subject: [PATCH 28/44] fix x11 implementation. --- src/Avalonia.X11/X11Window.cs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index eb1759aadd..97ba99c3dd 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -41,8 +41,6 @@ namespace Avalonia.X11 private IntPtr _renderHandle; private bool _mapped; private bool _wasMappedAtLeastOnce = false; - private HashSet _transientChildren = new HashSet(); - private X11Window _transientParent; private double? _scalingOverride; private bool _disabled; @@ -775,26 +773,19 @@ namespace Avalonia.X11 bool ActivateTransientChildIfNeeded() { - if(_disabled) + if (_disabled) { + GotInputWhenDisabled?.Invoke(); return false; } - if (_transientChildren.Count == 0) - return false; - var child = _transientChildren.First(); - if (!child.ActivateTransientChildIfNeeded()) - child.Activate(); return true; } - + void SetTransientParent(X11Window window, bool informServer = true) - { - _transientParent?._transientChildren.Remove(this); - _transientParent = window; - _transientParent?._transientChildren.Add(this); + { if (informServer) - SetTransientForHint(_transientParent?._handle); + SetTransientForHint(window?._handle); } void SetTransientForHint(IntPtr? parent) From 4e0e72abf66d91bf2c4be6353eaeda13f12e4764 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 12:59:46 -0300 Subject: [PATCH 29/44] remove redundant call --- src/Avalonia.Controls/Window.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 9e0dae2e1f..74b12cc2be 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -389,16 +389,6 @@ namespace Avalonia.Controls var args = new CancelEventArgs(); OnClosing(args); - if(!args.Cancel) - { - if (Owner is Window owner) - { - owner.RemoveChild(this); - } - - Owner = null; - } - return args.Cancel; } From 313a292fc4d37c1f1130a571764c59929766648c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 13:36:48 -0300 Subject: [PATCH 30/44] extend api so that when a parent is set you can state if the current window becomes modal. --- src/Avalonia.Controls/Platform/IWindowImpl.cs | 3 ++- src/Avalonia.Controls/Window.cs | 2 +- src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs | 2 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 2 +- src/Avalonia.Native/WindowImpl.cs | 2 +- src/Avalonia.X11/X11Window.cs | 2 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index cf31d30332..6bd64ad809 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -29,7 +29,8 @@ namespace Avalonia.Platform /// Sets the parent of the window. /// /// The parent . - void SetParent(IWindowImpl parent); + /// If this window is modal or not. + void SetParent(IWindowImpl parent, bool isModal); /// /// Disables the window for example when a modal dialog is open. diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 74b12cc2be..b45e8cb4c4 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -539,7 +539,7 @@ namespace Avalonia.Controls using (BeginAutoSizing()) { - PlatformImpl.SetParent(owner.PlatformImpl); + PlatformImpl.SetParent(owner.PlatformImpl, true); Owner = owner; owner.AddChild(this); PlatformImpl?.Show(); diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 844489ef97..b7299fc9e4 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -117,7 +117,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void SetParent(IWindowImpl parent) + public void SetParent(IWindowImpl parent, bool isModal) { } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 484cf3bc97..3512320dc0 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -130,7 +130,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void SetParent(IWindowImpl parent) + public void SetParent(IWindowImpl parent, bool isModal) { } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 25cccb6140..bbacff7988 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -114,7 +114,7 @@ namespace Avalonia.Native public Action GotInputWhenDisabled { get; set; } - public void SetParent(IWindowImpl parent) + public void SetParent(IWindowImpl parent, bool isModal) { } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 97ba99c3dd..a0da7a282a 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -1033,7 +1033,7 @@ namespace Avalonia.X11 ChangeWMAtoms(value, _x11.Atoms._NET_WM_STATE_ABOVE); } - public void SetParent(IWindowImpl parent) + public void SetParent(IWindowImpl parent, bool isModal) { SetTransientParent((X11Window)parent); } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 192fa2ec0e..8ba3a87b8f 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -351,7 +351,7 @@ namespace Avalonia.Win32 public Action GotInputWhenDisabled { get; set; } - public void SetParent(IWindowImpl parent) + public void SetParent(IWindowImpl parent, bool isModal) { _parent = (WindowImpl)parent; SetOwnerHandle(_parent._hwnd); From 86ac0ff5a5e1687fac162744bbf17b69c62e9826 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 14:40:52 -0300 Subject: [PATCH 31/44] OSX implement SetParent --- native/Avalonia.Native/inc/avalonia-native.h | 5 +- native/Avalonia.Native/src/OSX/window.h | 3 +- native/Avalonia.Native/src/OSX/window.mm | 78 +++++++++++-------- src/Avalonia.Controls/Platform/IWindowImpl.cs | 3 +- src/Avalonia.Controls/Window.cs | 2 +- .../Remote/PreviewerWindowImpl.cs | 2 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 2 +- src/Avalonia.Native/PopupImpl.cs | 5 ++ src/Avalonia.Native/WindowImpl.cs | 9 ++- src/Avalonia.X11/X11Window.cs | 2 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- 11 files changed, 68 insertions(+), 45 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index c2a9faf70c..7782165263 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -267,7 +267,8 @@ AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase { - virtual HRESULT ShowDialog (IAvnWindow* parent) = 0; + virtual HRESULT SetEnabled (bool enable) = 0; + virtual HRESULT SetParent (IAvnWindow* parent) = 0; virtual HRESULT SetCanResize(bool value) = 0; virtual HRESULT SetDecorations(SystemDecorations value) = 0; virtual HRESULT SetTitle (void* utf8Title) = 0; @@ -309,6 +310,8 @@ AVNCOM(IAvnWindowEvents, 06) : IAvnWindowBaseEvents virtual bool Closing () = 0; virtual void WindowStateChanged (AvnWindowState state) = 0; + + virtual void GotInputWhenDisabled () = 0; }; AVNCOM(IAvnMacOptions, 07) : IUnknown diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 163db36800..ca60914526 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -19,8 +19,7 @@ class WindowBaseImpl; -(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) restoreParentWindow; -(bool) shouldTryToHandleEvents; --(bool) isModal; --(void) setModal: (bool) isModal; +-(void) setEnabled: (bool) enable; -(void) showAppMenuOnly; -(void) showWindowMenuWithAppMenu; -(void) applyMenu:(NSMenu* _Nullable)menu; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 06b0c50456..e0f37d9055 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -29,6 +29,7 @@ public: NSString* _lastTitle; IAvnMenu* _mainMenu; bool _shown; + bool _isChild; WindowBaseImpl(IAvnWindowBaseEvents* events, IAvnGlContext* gl) { @@ -36,6 +37,7 @@ public: _mainMenu = nullptr; BaseEvents = events; _glContext = gl; + _isChild = false; renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext: gl]; View = [[AvnView alloc] initWithParent:this]; @@ -497,12 +499,7 @@ private: virtual HRESULT Show () override { @autoreleasepool - { - if([Window parentWindow] != nil) - [[Window parentWindow] removeChildWindow:Window]; - - [Window setModal:FALSE]; - + { WindowBaseImpl::Show(); HideOrShowTrafficLights(); @@ -511,7 +508,16 @@ private: } } - virtual HRESULT ShowDialog (IAvnWindow* parent) override + virtual HRESULT SetEnabled (bool enable) override + { + @autoreleasepool + { + [Window setEnabled:enable]; + return S_OK; + } + } + + virtual HRESULT SetParent (IAvnWindow* parent) override { @autoreleasepool { @@ -522,12 +528,10 @@ private: if(cparent == nullptr) return E_INVALIDARG; - [Window setModal:TRUE]; - + _isChild = true; [cparent->Window addChildWindow:Window ordered:NSWindowAbove]; - WindowBaseImpl::Show(); - HideOrShowTrafficLights(); + UpdateStyle(); return S_OK; } @@ -883,15 +887,15 @@ protected: switch (_decorations) { case SystemDecorationsNone: - s = s | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable; + s = s | NSWindowStyleMaskFullSizeContentView; break; case SystemDecorationsBorderOnly: - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable; + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; break; case SystemDecorationsFull: - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless; + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskBorderless; if(_canResize) { @@ -900,6 +904,10 @@ protected: break; } + if(!_isChild) + { + s |= NSWindowStyleMaskMiniaturizable; + } return s; } }; @@ -1089,7 +1097,15 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type { if([self ignoreUserInput]) + { + auto window = dynamic_cast(_parent.getRaw()); + + if(window != nullptr) + { + window->WindowEvents->GotInputWhenDisabled(); + } return; + } [self becomeFirstResponder]; auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; @@ -1234,7 +1250,16 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type { if([self ignoreUserInput]) + { + auto window = dynamic_cast(_parent.getRaw()); + + if(window != nullptr) + { + window->WindowEvents->GotInputWhenDisabled(); + } return; + } + auto key = s_KeyMap[[event keyCode]]; auto timestamp = [event timestamp] * 1000; @@ -1416,7 +1441,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent ComPtr _parent; bool _canBecomeKeyAndMain; bool _closed; - bool _isModal; + bool _isEnabled; AvnMenu* _menu; double _lastScaling; } @@ -1538,6 +1563,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _parent = parent; [self setDelegate:self]; _closed = false; + _isEnabled = true; _lastScaling = [self backingScaleFactor]; [self setOpaque:NO]; @@ -1604,28 +1630,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent -(bool)shouldTryToHandleEvents { - for(NSWindow* uch in [self childWindows]) - { - auto ch = objc_cast(uch); - if(ch == nil) - continue; - - if(![ch isModal]) - continue; - - return FALSE; - } - return TRUE; -} - --(bool) isModal -{ - return _isModal; + return _isEnabled; } --(void) setModal: (bool) isModal +-(void) setEnabled:(bool)enable { - _isModal = isModal; + _isEnabled = enable; } -(void)makeKeyWindow diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 6bd64ad809..cf31d30332 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -29,8 +29,7 @@ namespace Avalonia.Platform /// Sets the parent of the window. /// /// The parent . - /// If this window is modal or not. - void SetParent(IWindowImpl parent, bool isModal); + void SetParent(IWindowImpl parent); /// /// Disables the window for example when a modal dialog is open. diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index b45e8cb4c4..74b12cc2be 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -539,7 +539,7 @@ namespace Avalonia.Controls using (BeginAutoSizing()) { - PlatformImpl.SetParent(owner.PlatformImpl, true); + PlatformImpl.SetParent(owner.PlatformImpl); Owner = owner; owner.AddChild(this); PlatformImpl?.Show(); diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index b7299fc9e4..844489ef97 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -117,7 +117,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void SetParent(IWindowImpl parent, bool isModal) + public void SetParent(IWindowImpl parent) { } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 3512320dc0..484cf3bc97 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -130,7 +130,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void SetParent(IWindowImpl parent, bool isModal) + public void SetParent(IWindowImpl parent) { } diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index b7eec51c85..e4ee293757 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -43,6 +43,11 @@ namespace Avalonia.Native _parent = parent; } + public void GotInputWhenDisabled() + { + // NOP on Popup + } + bool IAvnWindowEvents.Closing() { return true; diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index bbacff7988..2d084bfe24 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -52,6 +52,11 @@ namespace Avalonia.Native { _parent.WindowStateChanged?.Invoke((WindowState)state); } + + void IAvnWindowEvents.GotInputWhenDisabled () + { + _parent.GotInputWhenDisabled?.Invoke(); + } } public IAvnWindow Native => _native; @@ -114,12 +119,14 @@ namespace Avalonia.Native public Action GotInputWhenDisabled { get; set; } - public void SetParent(IWindowImpl parent, bool isModal) + public void SetParent(IWindowImpl parent) { + _native.SetParent(((WindowImpl)parent).Native); } public void SetEnabled(bool enable) { + _native.SetEnabled(enable); } } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index a0da7a282a..97ba99c3dd 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -1033,7 +1033,7 @@ namespace Avalonia.X11 ChangeWMAtoms(value, _x11.Atoms._NET_WM_STATE_ABOVE); } - public void SetParent(IWindowImpl parent, bool isModal) + public void SetParent(IWindowImpl parent) { SetTransientParent((X11Window)parent); } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 8ba3a87b8f..192fa2ec0e 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -351,7 +351,7 @@ namespace Avalonia.Win32 public Action GotInputWhenDisabled { get; set; } - public void SetParent(IWindowImpl parent, bool isModal) + public void SetParent(IWindowImpl parent) { _parent = (WindowImpl)parent; SetOwnerHandle(_parent._hwnd); From 0e59c05b550903b6637d7f07e2ba8d225e14f711 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 15:10:45 -0300 Subject: [PATCH 32/44] implement closing chain logic. --- src/Avalonia.Controls/Window.cs | 54 ++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 74b12cc2be..1159fc4928 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -358,7 +358,7 @@ namespace Avalonia.Controls bool close = true; try - { + { if (!ignoreCancel && HandleClosing()) { close = false; @@ -369,16 +369,27 @@ namespace Avalonia.Controls { if (close) { - if (Owner is Window owner) - { - owner.RemoveChild(this); - } + CloseInternal(); + } + } + } - Owner = null; + private void CloseInternal () + { + foreach(var child in _children) + { + // if we HandleClosing() before then there will be no children. + child.CloseInternal(); + } - PlatformImpl?.Dispose(); - } + if (Owner is Window owner) + { + owner.RemoveChild(this); } + + Owner = null; + + PlatformImpl?.Dispose(); } /// @@ -386,10 +397,31 @@ namespace Avalonia.Controls /// protected virtual bool HandleClosing() { - var args = new CancelEventArgs(); - OnClosing(args); + bool canClose = true; - return args.Cancel; + foreach(var child in _children) + { + if(!child.HandleClosing()) + { + child.CloseInternal(); + } + else + { + canClose = false; + } + } + + if (canClose) + { + var args = new CancelEventArgs(); + OnClosing(args); + + return args.Cancel; + } + else + { + return !canClose; + } } protected virtual void HandleWindowStateChanged(WindowState state) From 97ae32c619c6707e74a55d55c7d64e305b57512a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 15:14:18 -0300 Subject: [PATCH 33/44] fix collection modified exception. --- src/Avalonia.Controls/Window.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 1159fc4928..086615d7d9 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -399,7 +399,7 @@ namespace Avalonia.Controls { bool canClose = true; - foreach(var child in _children) + foreach(var child in _children.ToList()) { if(!child.HandleClosing()) { From cb57a4313b0e294723cbad68a54e6e72058ab841 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 15:17:25 -0300 Subject: [PATCH 34/44] make owner property readonly. --- src/Avalonia.Controls/WindowBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index bb63d1b353..afc01db506 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -109,7 +109,7 @@ namespace Avalonia.Controls public WindowBase Owner { get { return _owner; } - set { SetAndRaise(OwnerProperty, ref _owner, value); } + protected set { SetAndRaise(OwnerProperty, ref _owner, value); } } /// From 84fafd654767e5265d45eadc7037c8043751d3a1 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 21:19:42 -0300 Subject: [PATCH 35/44] remove isChild flag. --- native/Avalonia.Native/src/OSX/window.mm | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index e0f37d9055..daaee62e34 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -29,7 +29,6 @@ public: NSString* _lastTitle; IAvnMenu* _mainMenu; bool _shown; - bool _isChild; WindowBaseImpl(IAvnWindowBaseEvents* events, IAvnGlContext* gl) { @@ -37,7 +36,6 @@ public: _mainMenu = nullptr; BaseEvents = events; _glContext = gl; - _isChild = false; renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext: gl]; View = [[AvnView alloc] initWithParent:this]; @@ -528,7 +526,6 @@ private: if(cparent == nullptr) return E_INVALIDARG; - _isChild = true; [cparent->Window addChildWindow:Window ordered:NSWindowAbove]; UpdateStyle(); @@ -904,7 +901,7 @@ protected: break; } - if(!_isChild) + if([Window parentWindow] == nullptr) { s |= NSWindowStyleMaskMiniaturizable; } From d5cdb966185c631c390f96824e3cec17202fdad8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 21:22:07 -0300 Subject: [PATCH 36/44] remove duplicate code. --- native/Avalonia.Native/src/OSX/window.mm | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index daaee62e34..fed2176580 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1086,8 +1086,19 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (bool) ignoreUserInput { auto parentWindow = objc_cast([self window]); + if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents]) + { + auto window = dynamic_cast(_parent.getRaw()); + + if(window != nullptr) + { + window->WindowEvents->GotInputWhenDisabled(); + } + return TRUE; + } + return FALSE; } @@ -1095,12 +1106,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { if([self ignoreUserInput]) { - auto window = dynamic_cast(_parent.getRaw()); - - if(window != nullptr) - { - window->WindowEvents->GotInputWhenDisabled(); - } return; } @@ -1248,12 +1253,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { if([self ignoreUserInput]) { - auto window = dynamic_cast(_parent.getRaw()); - - if(window != nullptr) - { - window->WindowEvents->GotInputWhenDisabled(); - } return; } From 578443d6e00ec0a1cb134ce48cf01ecbe7f96ad0 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 21:24:30 -0300 Subject: [PATCH 37/44] nits --- src/Avalonia.Controls/Window.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 086615d7d9..4b6df20363 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -358,7 +358,7 @@ namespace Avalonia.Controls bool close = true; try - { + { if (!ignoreCancel && HandleClosing()) { close = false; @@ -369,7 +369,7 @@ namespace Avalonia.Controls { if (close) { - CloseInternal(); + CloseInternal(); } } } From c928700febf564fcd8408d64dda61f838c4a09c9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 21:24:44 -0300 Subject: [PATCH 38/44] make copy of list before enumerating. --- src/Avalonia.Controls/Window.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 4b6df20363..07e3d78650 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -376,7 +376,7 @@ namespace Avalonia.Controls private void CloseInternal () { - foreach(var child in _children) + foreach(var child in _children.ToList()) { // if we HandleClosing() before then there will be no children. child.CloseInternal(); From 2501698af1b1c5c65467bf1fb704bb5be69144b7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 21:31:51 -0300 Subject: [PATCH 39/44] whitespace --- src/Avalonia.Controls/Window.cs | 14 +++++++------- src/Avalonia.Native/WindowImpl.cs | 6 +++--- src/Windows/Avalonia.Win32/WindowImpl.cs | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 07e3d78650..194ebeb999 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -69,7 +69,7 @@ namespace Avalonia.Controls /// public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot { - private List _children = new List(); + private List _children = new List(); /// /// Defines the property. @@ -374,9 +374,9 @@ namespace Avalonia.Controls } } - private void CloseInternal () + private void CloseInternal() { - foreach(var child in _children.ToList()) + foreach (var child in _children.ToList()) { // if we HandleClosing() before then there will be no children. child.CloseInternal(); @@ -399,9 +399,9 @@ namespace Avalonia.Controls { bool canClose = true; - foreach(var child in _children.ToList()) + foreach (var child in _children.ToList()) { - if(!child.HandleClosing()) + if (!child.HandleClosing()) { child.CloseInternal(); } @@ -454,7 +454,7 @@ namespace Avalonia.Controls if (Owner is Window owner) { - owner.RemoveChild(this); + owner.RemoveChild(this); } Owner = null; @@ -720,7 +720,7 @@ namespace Avalonia.Controls if (Owner is Window owner) { - owner.RemoveChild(this); + owner.RemoveChild(this); } Owner = null; diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 2d084bfe24..e91445000a 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -40,7 +40,7 @@ namespace Avalonia.Native bool IAvnWindowEvents.Closing() { - if(_parent.Closing != null) + if (_parent.Closing != null) { return _parent.Closing(); } @@ -53,7 +53,7 @@ namespace Avalonia.Native _parent.WindowStateChanged?.Invoke((WindowState)state); } - void IAvnWindowEvents.GotInputWhenDisabled () + void IAvnWindowEvents.GotInputWhenDisabled() { _parent.GotInputWhenDisabled?.Invoke(); } @@ -71,7 +71,7 @@ namespace Avalonia.Native _native.Decorations = (Interop.SystemDecorations)enabled; } - public void SetTitleBarColor (Avalonia.Media.Color color) + public void SetTitleBarColor(Avalonia.Media.Color color) { _native.SetTitleBarColor(new AvnColor { Alpha = color.A, Red = color.R, Green = color.G, Blue = color.B }); } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 192fa2ec0e..9bf59a563c 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -44,7 +44,7 @@ namespace Avalonia.Win32 private readonly ManagedWindowResizeDragHelper _managedDrag; #endif - private const WindowStyles WindowStateMask = (WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE); + private const WindowStyles WindowStateMask = (WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE); private readonly TouchDevice _touchDevice; private readonly MouseDevice _mouseDevice; private readonly ManagedDeferredRendererLock _rendererLock; @@ -660,7 +660,7 @@ namespace Avalonia.Win32 private void SetOwnerHandle(IntPtr handle) => SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, handle); - private WindowStyles GetWindowStateStyles () + private WindowStyles GetWindowStateStyles() { return GetStyle() & WindowStateMask; } From 1474670710eea4b7f394de4c9bdc557bcb879015 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 May 2020 21:33:51 -0300 Subject: [PATCH 40/44] remove method. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 9bf59a563c..cc4c12ec3c 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -354,7 +354,7 @@ namespace Avalonia.Win32 public void SetParent(IWindowImpl parent) { _parent = (WindowImpl)parent; - SetOwnerHandle(_parent._hwnd); + SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, _parent._hwnd); } public void SetEnabled(bool enable) => EnableWindow(_hwnd, enable); @@ -656,9 +656,7 @@ namespace Avalonia.Win32 SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); } } - } - - private void SetOwnerHandle(IntPtr handle) => SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, handle); + } private WindowStyles GetWindowStateStyles() { From 0f353769ec5b74540819315cac67f039a7b3681c Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 15 May 2020 21:20:43 +0300 Subject: [PATCH 41/44] X11Window cleanup --- src/Avalonia.X11/X11Window.cs | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 97ba99c3dd..cdd7a64d5d 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -746,7 +746,6 @@ namespace Avalonia.X11 void Cleanup() { - SetTransientParent(null, false); if (_xic != IntPtr.Zero) { XDestroyIC(_xic); @@ -782,27 +781,15 @@ namespace Avalonia.X11 return true; } - void SetTransientParent(X11Window window, bool informServer = true) - { - if (informServer) - SetTransientForHint(window?._handle); - } - - void SetTransientForHint(IntPtr? parent) + public void SetParent(IWindowImpl parent) { - if (parent == null || parent == IntPtr.Zero) + if (parent == null || parent.Handle == null || parent.Handle.Handle == IntPtr.Zero) XDeleteProperty(_x11.Display, _handle, _x11.Atoms.XA_WM_TRANSIENT_FOR); else - XSetTransientForHint(_x11.Display, _handle, parent.Value); + XSetTransientForHint(_x11.Display, _handle, parent.Handle.Handle); } public void Show() - { - SetTransientParent(null); - ShowCore(); - } - - void ShowCore() { _wasMappedAtLeastOnce = true; XMapWindow(_x11.Display, _handle); @@ -811,7 +798,6 @@ namespace Avalonia.X11 public void Hide() => XUnmapWindow(_x11.Display, _handle); - public Point PointToClient(PixelPoint point) => new Point((point.X - Position.X) / Scaling, (point.Y - Position.Y) / Scaling); public PixelPoint PointToScreen(Point point) => new PixelPoint( @@ -1032,12 +1018,7 @@ namespace Avalonia.X11 { ChangeWMAtoms(value, _x11.Atoms._NET_WM_STATE_ABOVE); } - - public void SetParent(IWindowImpl parent) - { - SetTransientParent((X11Window)parent); - } - + public void SetEnabled(bool enable) { _disabled = !enable; From 8bcafad8a3bd1730afd6e94126a1ca9a07c4172f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 15 May 2020 21:49:29 +0300 Subject: [PATCH 42/44] Fixed ActivateTransientChildIfNeeded --- src/Avalonia.X11/X11Window.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index cdd7a64d5d..643b037b3f 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -775,10 +775,10 @@ namespace Avalonia.X11 if (_disabled) { GotInputWhenDisabled?.Invoke(); - return false; + return true; } - return true; + return false; } public void SetParent(IWindowImpl parent) From 2f1974684e82ac6dc1f757c507a4a0e94953dca9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 15 May 2020 15:58:52 -0300 Subject: [PATCH 43/44] fix unit test. --- tests/Avalonia.Controls.UnitTests/WindowTests.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index e99be9cfd2..80c8a34ffd 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -322,10 +322,9 @@ namespace Avalonia.Controls.UnitTests { var window = new Window(); window.WindowStartupLocation = WindowStartupLocation.CenterOwner; - window.Position = new PixelPoint(60, 40); - window.Owner = parentWindow; + window.Position = new PixelPoint(60, 40); - window.Show(); + window.ShowDialog(parentWindow); var expectedPosition = new PixelPoint( (int)(parentWindow.Position.X + parentWindow.ClientSize.Width / 2 - window.ClientSize.Width / 2), From 234664011c88711610326751c4b5823ff439d1f8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 16 May 2020 16:21:12 +0200 Subject: [PATCH 44/44] Fixed typo. --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 896f922fcd..cfe47a09d5 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -1,5 +1,4 @@ -// (c) Copyright Microsoft Corporation. -// This source is subject to the Microsoft Public License (Ms-PL). +// This source is subject to the Microsoft Public License (Ms-PL). // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. @@ -767,7 +766,7 @@ namespace Avalonia.Controls /// /// ItemsProperty property changed handler. /// - /// AvaloniaPropertyChangedEventArgsdEventArgs. + /// The event arguments. private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e) { if (!_areHandlersSuspended)