diff --git a/Documentation/build.md b/Documentation/build.md index 56b028206d..8c2ef57b54 100644 --- a/Documentation/build.md +++ b/Documentation/build.md @@ -36,7 +36,7 @@ Avalonia requires [CastXML](https://github.com/CastXML/CastXML) for XML processi On macOS: ``` -brew install castxml +brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb ``` On Debian based Linux (Debian, Ubuntu, Mint, etc): 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..fed2176580 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -497,12 +497,7 @@ private: virtual HRESULT Show () override { @autoreleasepool - { - if([Window parentWindow] != nil) - [[Window parentWindow] removeChildWindow:Window]; - - [Window setModal:FALSE]; - + { WindowBaseImpl::Show(); HideOrShowTrafficLights(); @@ -511,7 +506,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 +526,9 @@ private: if(cparent == nullptr) return E_INVALIDARG; - [Window setModal:TRUE]; - [cparent->Window addChildWindow:Window ordered:NSWindowAbove]; - WindowBaseImpl::Show(); - HideOrShowTrafficLights(); + UpdateStyle(); return S_OK; } @@ -883,15 +884,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 +901,10 @@ protected: break; } + if([Window parentWindow] == nullptr) + { + s |= NSWindowStyleMaskMiniaturizable; + } return s; } }; @@ -1081,15 +1086,28 @@ 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; } - (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type { if([self ignoreUserInput]) + { return; + } [self becomeFirstResponder]; auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; @@ -1234,7 +1252,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type { if([self ignoreUserInput]) + { return; + } + auto key = s_KeyMap[[event keyCode]]; auto timestamp = [event timestamp] * 1000; @@ -1416,7 +1437,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent ComPtr _parent; bool _canBecomeKeyAndMain; bool _closed; - bool _isModal; + bool _isEnabled; AvnMenu* _menu; double _lastScaling; } @@ -1538,6 +1559,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _parent = parent; [self setDelegate:self]; _closed = false; + _isEnabled = true; _lastScaling = [self backingScaleFactor]; [self setOpaque:NO]; @@ -1604,28 +1626,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.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index fa1e955153..9e9b84537b 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -1,10 +1,10 @@ using System; +using System.Collections; using System.Collections.Generic; -using System.Linq; -using System.Reactive.Linq; -using Avalonia.Collections; +using System.Collections.Specialized; using Avalonia.Data; -using Avalonia.Animation.Animators; + +#nullable enable namespace Avalonia.Animation { @@ -13,9 +13,24 @@ namespace Avalonia.Animation /// public class Animatable : AvaloniaObject { + /// + /// Defines the property. + /// public static readonly StyledProperty ClockProperty = AvaloniaProperty.Register(nameof(Clock), inherits: true); + /// + /// Defines the property. + /// + public static readonly StyledProperty TransitionsProperty = + AvaloniaProperty.Register(nameof(Transitions)); + + private bool _transitionsEnabled = true; + private Dictionary? _transitionState; + + /// + /// Gets or sets the clock which controls the animations on the control. + /// public IClock Clock { get => GetValue(ClockProperty); @@ -23,72 +38,194 @@ namespace Avalonia.Animation } /// - /// Defines the property. + /// Gets or sets the property transitions for the control. /// - public static readonly DirectProperty TransitionsProperty = - AvaloniaProperty.RegisterDirect( - nameof(Transitions), - o => o.Transitions, - (o, v) => o.Transitions = v); + public Transitions? Transitions + { + get => GetValue(TransitionsProperty); + set => SetValue(TransitionsProperty, value); + } - private Transitions _transitions; + /// + /// Enables transitions for the control. + /// + /// + /// This method should not be called from user code, it will be called automatically by the framework + /// when a control is added to the visual tree. + /// + protected void EnableTransitions() + { + if (!_transitionsEnabled) + { + _transitionsEnabled = true; - private Dictionary _previousTransitions; + if (Transitions is object) + { + AddTransitions(Transitions); + } + } + } /// - /// Gets or sets the property transitions for the control. + /// Disables transitions for the control. /// - public Transitions Transitions + /// + /// This method should not be called from user code, it will be called automatically by the framework + /// when a control is added to the visual tree. + /// + protected void DisableTransitions() + { + if (_transitionsEnabled) + { + _transitionsEnabled = false; + + if (Transitions is object) + { + RemoveTransitions(Transitions); + } + } + } + + protected sealed override void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) { - get + if (change.Property == TransitionsProperty && change.IsEffectiveValueChange) { - if (_transitions is null) - _transitions = new Transitions(); + var oldTransitions = change.OldValue.GetValueOrDefault(); + var newTransitions = change.NewValue.GetValueOrDefault(); - if (_previousTransitions is null) - _previousTransitions = new Dictionary(); + if (oldTransitions is object) + { + oldTransitions.CollectionChanged -= TransitionsCollectionChanged; + RemoveTransitions(oldTransitions); + } - return _transitions; + if (newTransitions is object) + { + newTransitions.CollectionChanged += TransitionsCollectionChanged; + AddTransitions(newTransitions); + } } - set + else if (_transitionsEnabled && + Transitions is object && + _transitionState is object && + !change.Property.IsDirect && + change.Priority > BindingPriority.Animation) { - if (value is null) - return; + foreach (var transition in Transitions) + { + if (transition.Property == change.Property) + { + var state = _transitionState[transition]; + var oldValue = state.BaseValue; + var newValue = GetAnimationBaseValue(transition.Property); - if (_previousTransitions is null) - _previousTransitions = new Dictionary(); + if (!Equals(oldValue, newValue)) + { + state.BaseValue = newValue; - SetAndRaise(TransitionsProperty, ref _transitions, value); + // We need to transition from the current animated value if present, + // instead of the old base value. + var animatedValue = GetValue(transition.Property); + + if (!Equals(newValue, animatedValue)) + { + oldValue = animatedValue; + } + + state.Instance?.Dispose(); + state.Instance = transition.Apply( + this, + Clock ?? AvaloniaLocator.Current.GetService(), + oldValue, + newValue); + return; + } + } + } + } + + base.OnPropertyChangedCore(change); + } + + private void TransitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (!_transitionsEnabled) + { + return; + } + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + AddTransitions(e.NewItems); + break; + case NotifyCollectionChangedAction.Remove: + RemoveTransitions(e.OldItems); + break; + case NotifyCollectionChangedAction.Replace: + RemoveTransitions(e.OldItems); + AddTransitions(e.NewItems); + break; + case NotifyCollectionChangedAction.Reset: + throw new NotSupportedException("Transitions collection cannot be reset."); } } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + private void AddTransitions(IList items) { - if (_transitions is null || _previousTransitions is null || priority == BindingPriority.Animation) + if (!_transitionsEnabled) + { return; + } + + _transitionState ??= new Dictionary(); - // PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations). - foreach (var transition in _transitions) + for (var i = 0; i < items.Count; ++i) { - if (transition.Property == property) + var t = (ITransition)items[i]; + + _transitionState.Add(t, new TransitionState { - if (_previousTransitions.TryGetValue(property, out var dispose)) - dispose.Dispose(); + BaseValue = GetAnimationBaseValue(t.Property), + }); + } + } - var instance = transition.Apply( - this, - Clock ?? Avalonia.Animation.Clock.GlobalClock, - oldValue.GetValueOrDefault(), - newValue.GetValueOrDefault()); + private void RemoveTransitions(IList items) + { + if (_transitionState is null) + { + return; + } - _previousTransitions[property] = instance; - return; + for (var i = 0; i < items.Count; ++i) + { + var t = (ITransition)items[i]; + + if (_transitionState.TryGetValue(t, out var state)) + { + state.Instance?.Dispose(); + _transitionState.Remove(t); } } } + + private object GetAnimationBaseValue(AvaloniaProperty property) + { + var value = this.GetBaseValue(property, BindingPriority.LocalValue); + + if (value == AvaloniaProperty.UnsetValue) + { + value = GetValue(property); + } + + return value; + } + + private class TransitionState + { + public IDisposable? Instance { get; set; } + public object? BaseValue { get; set; } + } } } diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index efbbed51b5..ad2001d621 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/src/Avalonia.Animation/TransitionInstance.cs @@ -19,6 +19,8 @@ namespace Avalonia.Animation public TransitionInstance(IClock clock, TimeSpan Duration) { + clock = clock ?? throw new ArgumentNullException(nameof(clock)); + _duration = Duration; _baseClock = clock; } diff --git a/src/Avalonia.Animation/Transitions.cs b/src/Avalonia.Animation/Transitions.cs index 2741039ebc..6687a2902d 100644 --- a/src/Avalonia.Animation/Transitions.cs +++ b/src/Avalonia.Animation/Transitions.cs @@ -1,4 +1,6 @@ +using System; using Avalonia.Collections; +using Avalonia.Threading; namespace Avalonia.Animation { @@ -13,6 +15,17 @@ namespace Avalonia.Animation public Transitions() { ResetBehavior = ResetBehavior.Remove; + Validate = ValidateTransition; + } + + private void ValidateTransition(ITransition obj) + { + Dispatcher.UIThread.VerifyAccess(); + + if (obj.Property.IsDirect) + { + throw new InvalidOperationException("Cannot animate a direct property."); + } } } } diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index ed36e6da43..f387d7e0b6 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,51 @@ namespace Avalonia return property.GetDefaultValue(GetType()); } + protected internal void RaisePropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + VerifyAccess(); + + if (change.IsEffectiveValueChange) + { + 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 + { + if (change.IsEffectiveValueChange) + { + 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/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); } 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 3a1e612a05..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. /// - /// AvaloniaPropertyChangedEventArgs. + /// The event arguments. private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e) { if (!_areHandlersSuspended) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 136e8ed851..b54eb2ac57 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 7945d63b06..44f66d397a 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 7cd0230b36..0f53dc1364 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -510,17 +510,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 == 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 0cfea2c68b..6d9f6b8b77 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -136,17 +136,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/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) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 9cbde72f7f..09f86f462c 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -75,6 +75,9 @@ namespace Avalonia.Controls.Presenters static TextPresenter() { AffectsRender(SelectionBrushProperty); + AffectsMeasure(TextProperty, PasswordCharProperty, + TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty, + TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty); Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed, TextAlignmentProperty.Changed, TextWrappingProperty.Changed, @@ -284,8 +287,6 @@ namespace Avalonia.Controls.Presenters protected void InvalidateFormattedText() { _formattedText = null; - - InvalidateMeasure(); } /// @@ -301,13 +302,15 @@ namespace Avalonia.Controls.Presenters context.FillRectangle(background, new Rect(Bounds.Size)); } - FormattedText.Constraint = Bounds.Size; - context.DrawText(Foreground, new Point(), FormattedText); } public override void Render(DrawingContext context) { + FormattedText.Constraint = Bounds.Size; + + _constraint = Bounds.Size; + var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; @@ -316,10 +319,6 @@ namespace Avalonia.Controls.Presenters var start = Math.Min(selectionStart, selectionEnd); var length = Math.Max(selectionStart, selectionEnd) - start; - // issue #600: set constraint before any FormattedText manipulation - // see base.Render(...) implementation - FormattedText.Constraint = _constraint; - var rects = FormattedText.HitTestTextRange(start, length); foreach (var rect in rects) diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index 2e6292b4b4..f6186a29a9 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/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 8fc1a55e68..c915dc70b6 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -455,13 +455,13 @@ namespace Avalonia.Controls.Primitives InternalEndInit(); } - 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 == SelectionModeProperty) + if (change.Property == SelectionModeProperty) { - var mode = newValue.GetValueOrDefault(); + var mode = change.NewValue.GetValueOrDefault(); Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple); Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected); } 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 9225625bf5..c7356f2b4d 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..069da6e9ac 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -375,37 +376,46 @@ 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 newDataSource = newEnumerable as ItemsSourceView; - if (newEnumerable != null && newDataSource == null) + var oldEnumerable = change.OldValue.GetValueOrDefault(); + var newEnumerable = change.NewValue.GetValueOrDefault(); + + if (oldEnumerable != newEnumerable) { - newDataSource = new ItemsSourceView(newEnumerable); - } + var newDataSource = newEnumerable as ItemsSourceView; + if (newEnumerable != null && newDataSource == null) + { + newDataSource = new ItemsSourceView(newEnumerable); + } - OnDataSourcePropertyChanged(ItemsSourceView, newDataSource); + 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) @@ -431,8 +441,16 @@ namespace Avalonia.Controls private int GetElementIndexImpl(IControl element) { - var virtInfo = TryGetVirtualizationInfo(element); - return _viewManager.GetElementIndex(virtInfo); + // Verify that element is actually a child of this ItemsRepeater + var parent = element.GetVisualParent(); + + if (parent == this) + { + var virtInfo = TryGetVirtualizationInfo(element); + return _viewManager.GetElementIndex(virtInfo); + } + + return -1; } private IControl GetElementFromIndexImpl(int index) diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index 7d005a30b4..4ed9cb3333 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -388,19 +388,24 @@ namespace Avalonia.Controls } case NotifyCollectionChangedAction.Reset: - if (_owner.ItemsSourceView.HasKeyIndexMapping) + // If we get multiple resets back to back before + // running layout, we dont have to clear all the elements again. + if (!_isDataSourceStableResetPending) { - _isDataSourceStableResetPending = true; - } + if (_owner.ItemsSourceView.HasKeyIndexMapping) + { + _isDataSourceStableResetPending = true; + } - // Walk through all the elements and make sure they are cleared, they will go into - // the stable id reset pool. - foreach (var element in _owner.Children) - { - var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); - if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate) + // Walk through all the elements and make sure they are cleared, they will go into + // the stable id reset pool. + foreach (var element in _owner.Children) { - _owner.ClearElementImpl(element); + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate) + { + _owner.ClearElementImpl(element); + } } } @@ -441,6 +446,9 @@ namespace Avalonia.Controls } _resetPool.Clear(); + + // Flush the realized indices once the stable reset pool is cleared to start fresh. + InvalidateRealizedIndicesHeldByLayout(); } } @@ -498,6 +506,10 @@ namespace Avalonia.Controls var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool(); UpdateElementIndex(element, virtInfo, index); + + // Update realized indices + _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index); + _lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index); } } @@ -519,6 +531,10 @@ namespace Avalonia.Controls _pinnedPool.RemoveAt(i); element = elementInfo.PinnedElement; elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool(); + + // Update realized indices + _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index); + _lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index); break; } } diff --git a/src/Avalonia.Controls/Shapes/Path.cs b/src/Avalonia.Controls/Shapes/Path.cs index 3fd84c0c7b..d0ffc27d20 100644 --- a/src/Avalonia.Controls/Shapes/Path.cs +++ b/src/Avalonia.Controls/Shapes/Path.cs @@ -1,5 +1,4 @@ using System; -using Avalonia.Data; using Avalonia.Media; namespace Avalonia.Controls.Shapes @@ -9,6 +8,8 @@ namespace Avalonia.Controls.Shapes public static readonly StyledProperty DataProperty = AvaloniaProperty.Register(nameof(Data)); + private EventHandler _geometryChangedHandler; + static Path() { AffectsGeometry(DataProperty); @@ -21,21 +22,48 @@ namespace Avalonia.Controls.Shapes set { SetValue(DataProperty, value); } } + private EventHandler GeometryChangedHandler => _geometryChangedHandler ??= GeometryChanged; + protected override Geometry CreateDefiningGeometry() => Data; + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + if (Data is object) + { + Data.Changed += GeometryChangedHandler; + } + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + if (Data is object) + { + Data.Changed -= GeometryChangedHandler; + } + } + private void DataChanged(AvaloniaPropertyChangedEventArgs e) { + if (VisualRoot is null) + { + return; + } + var oldGeometry = (Geometry)e.OldValue; var newGeometry = (Geometry)e.NewValue; if (oldGeometry is object) { - oldGeometry.Changed -= GeometryChanged; + oldGeometry.Changed -= GeometryChangedHandler; } if (newGeometry is object) { - newGeometry.Changed += GeometryChanged; + newGeometry.Changed += GeometryChangedHandler; } } diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 371b5d92f7..7d1525afc4 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -2,38 +2,67 @@ using System; using Avalonia.Collections; using Avalonia.Media; +#nullable enable + namespace Avalonia.Controls.Shapes { + /// + /// Provides a base class for shape elements, such as , and . + /// public abstract class Shape : Control { - public static readonly StyledProperty FillProperty = - AvaloniaProperty.Register(nameof(Fill)); + /// + /// Defines the property. + /// + public static readonly StyledProperty FillProperty = + AvaloniaProperty.Register(nameof(Fill)); + /// + /// Defines the property. + /// public static readonly StyledProperty StretchProperty = AvaloniaProperty.Register(nameof(Stretch)); - public static readonly StyledProperty StrokeProperty = - AvaloniaProperty.Register(nameof(Stroke)); + /// + /// Defines the property. + /// + public static readonly StyledProperty StrokeProperty = + AvaloniaProperty.Register(nameof(Stroke)); - public static readonly StyledProperty> StrokeDashArrayProperty = - AvaloniaProperty.Register>(nameof(StrokeDashArray)); + /// + /// Defines the property. + /// + public static readonly StyledProperty?> StrokeDashArrayProperty = + AvaloniaProperty.Register?>(nameof(StrokeDashArray)); + /// + /// Defines the property. + /// public static readonly StyledProperty StrokeDashOffsetProperty = AvaloniaProperty.Register(nameof(StrokeDashOffset)); + /// + /// Defines the property. + /// public static readonly StyledProperty StrokeThicknessProperty = AvaloniaProperty.Register(nameof(StrokeThickness)); + /// + /// Defines the property. + /// public static readonly StyledProperty StrokeLineCapProperty = AvaloniaProperty.Register(nameof(StrokeLineCap), PenLineCap.Flat); + /// + /// Defines the property. + /// public static readonly StyledProperty StrokeJoinProperty = AvaloniaProperty.Register(nameof(StrokeJoin), PenLineJoin.Miter); private Matrix _transform = Matrix.Identity; - private Geometry _definingGeometry; - private Geometry _renderedGeometry; - bool _calculateTransformOnArrange = false; + private Geometry? _definingGeometry; + private Geometry? _renderedGeometry; + private bool _calculateTransformOnArrange; static Shape() { @@ -43,7 +72,10 @@ namespace Avalonia.Controls.Shapes StrokeThicknessProperty, StrokeLineCapProperty, StrokeJoinProperty); } - public Geometry DefiningGeometry + /// + /// Gets a value that represents the of the shape. + /// + public Geometry? DefiningGeometry { get { @@ -56,13 +88,10 @@ namespace Avalonia.Controls.Shapes } } - public IBrush Fill - { - get { return GetValue(FillProperty); } - set { SetValue(FillProperty, value); } - } - - public Geometry RenderedGeometry + /// + /// Gets a value that represents the final rendered of the shape. + /// + public Geometry? RenderedGeometry { get { @@ -93,42 +122,72 @@ namespace Avalonia.Controls.Shapes } } + /// + /// Gets or sets the that specifies how the shape's interior is painted. + /// + public IBrush? Fill + { + get { return GetValue(FillProperty); } + set { SetValue(FillProperty, value); } + } + + /// + /// Gets or sets a enumeration value that describes how the shape fills its allocated space. + /// public Stretch Stretch { get { return GetValue(StretchProperty); } set { SetValue(StretchProperty, value); } } - public IBrush Stroke + /// + /// Gets or sets the that specifies how the shape's outline is painted. + /// + public IBrush? Stroke { get { return GetValue(StrokeProperty); } set { SetValue(StrokeProperty, value); } } - public AvaloniaList StrokeDashArray + /// + /// Gets or sets a collection of values that indicate the pattern of dashes and gaps that is used to outline shapes. + /// + public AvaloniaList? StrokeDashArray { get { return GetValue(StrokeDashArrayProperty); } set { SetValue(StrokeDashArrayProperty, value); } } + /// + /// Gets or sets a value that specifies the distance within the dash pattern where a dash begins. + /// public double StrokeDashOffset { get { return GetValue(StrokeDashOffsetProperty); } set { SetValue(StrokeDashOffsetProperty, value); } } + /// + /// Gets or sets the width of the shape outline. + /// public double StrokeThickness { get { return GetValue(StrokeThicknessProperty); } set { SetValue(StrokeThicknessProperty, value); } } + /// + /// Gets or sets a enumeration value that describes the shape at the ends of a line. + /// public PenLineCap StrokeLineCap { get { return GetValue(StrokeLineCapProperty); } set { SetValue(StrokeLineCapProperty, value); } } + /// + /// Gets or sets a enumeration value that specifies the type of join that is used at the vertices of a Shape. + /// public PenLineJoin StrokeJoin { get { return GetValue(StrokeJoinProperty); } @@ -170,12 +229,20 @@ namespace Avalonia.Controls.Shapes } } - protected abstract Geometry CreateDefiningGeometry(); + /// + /// Creates the shape's defining geometry. + /// + /// Defining of the shape. + protected abstract Geometry? CreateDefiningGeometry(); + /// + /// Invalidates the geometry of this shape. + /// protected void InvalidateGeometry() { _renderedGeometry = null; _definingGeometry = null; + InvalidateMeasure(); } @@ -321,8 +388,8 @@ namespace Avalonia.Controls.Shapes // portion changes. if (e.Property == BoundsProperty) { - var oldBounds = (Rect)e.OldValue; - var newBounds = (Rect)e.NewValue; + var oldBounds = (Rect)e.OldValue!; + var newBounds = (Rect)e.NewValue!; if (oldBounds.Size == newBounds.Size) { diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index f243372926..e92c8faf20 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/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index a55274afb3..95e7437838 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -486,13 +486,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 == SelectionModeProperty) + if (change.Property == SelectionModeProperty) { - var mode = newValue.GetValueOrDefault(); + var mode = change.NewValue.GetValueOrDefault(); Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple); Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected); } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 7dacf4b2af..474d845905 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; @@ -68,6 +69,8 @@ namespace Avalonia.Controls /// public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot { + private List _children = new List(); + /// /// Defines the property. /// @@ -131,7 +134,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 /// @@ -183,6 +186,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)); @@ -302,7 +306,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 /// @@ -323,7 +327,7 @@ namespace Avalonia.Controls /// /// Fired before a window is closed. /// - public event EventHandler Closing; + public event EventHandler Closing; /// /// Closes the window. @@ -365,19 +369,59 @@ namespace Avalonia.Controls { if (close) { - PlatformImpl?.Dispose(); + CloseInternal(); } } } + private void CloseInternal() + { + foreach (var child in _children.ToList()) + { + // if we HandleClosing() before then there will be no children. + child.CloseInternal(); + } + + if (Owner is Window owner) + { + owner.RemoveChild(this); + } + + Owner = null; + + PlatformImpl?.Dispose(); + } + /// /// Handles a closing notification from . /// protected virtual bool HandleClosing() { - var args = new CancelEventArgs(); - OnClosing(args); - return args.Cancel; + bool canClose = true; + + foreach (var child in _children.ToList()) + { + 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) @@ -407,6 +451,14 @@ namespace Avalonia.Controls using (BeginAutoSizing()) { Renderer?.Stop(); + + if (Owner is Window owner) + { + owner.RemoveChild(this); + } + + Owner = null; + PlatformImpl?.Hide(); } @@ -519,7 +571,10 @@ namespace Avalonia.Controls using (BeginAutoSizing()) { - PlatformImpl?.ShowDialog(owner.PlatformImpl); + PlatformImpl.SetParent(owner.PlatformImpl); + Owner = owner; + owner.AddChild(this); + PlatformImpl?.Show(); Renderer?.Start(); @@ -541,6 +596,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; @@ -631,6 +717,13 @@ namespace Avalonia.Controls RaiseEvent(new RoutedEventArgs(WindowClosedEvent)); base.HandleClosed(); + + if (Owner is Window owner) + { + owner.RemoveChild(this); + } + + Owner = null; } /// @@ -658,19 +751,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.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); } } /// 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/RemoteDesignerEntryPoint.cs b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs index 44d9a94f5a..e61fe82c41 100644 --- a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs +++ b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs @@ -234,18 +234,10 @@ namespace Avalonia.DesignerSupport.Remote } catch (Exception e) { - var xmlException = e as XmlException; - s_transport.Send(new UpdateXamlResultMessage { Error = e.ToString(), - Exception = new ExceptionDetails - { - ExceptionType = e.GetType().FullName, - Message = e.Message.ToString(), - LineNumber = xmlException?.LineNumber, - LinePosition = xmlException?.LinePosition, - } + Exception = new ExceptionDetails(e), }); } } 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 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/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs index 7f44c80a64..0d64f8dfd5 100644 --- a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs @@ -74,6 +74,7 @@ namespace Avalonia.Layout double lineSpacing, int maxItemsPerLine, ScrollOrientation orientation, + bool disableVirtualization, string layoutId) { _orientation.ScrollOrientation = orientation; @@ -95,14 +96,14 @@ namespace Avalonia.Layout _elementManager.OnBeginMeasure(orientation); int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId); - Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); - Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); + Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId); + Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId); if (isWrapping && IsReflowRequired()) { var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); _orientation.SetMinorStart(ref firstElementBounds, 0); _elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds); - Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); + Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId); } RaiseLineArranged(); @@ -273,6 +274,7 @@ namespace Avalonia.Layout double minItemSpacing, double lineSpacing, int maxItemsPerLine, + bool disableVirtualization, string layoutId) { if (anchorIndex != -1) @@ -288,7 +290,7 @@ namespace Avalonia.Layout bool lineNeedsReposition = false; while (_elementManager.IsIndexValidInData(currentIndex) && - ShouldContinueFillingUpSpace(previousIndex, direction)) + (disableVirtualization || ShouldContinueFillingUpSpace(previousIndex, direction))) { // Ensure layout element. _elementManager.EnsureElementRealized(direction == GenerateDirection.Forward, currentIndex, layoutId); diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index 9b8eb4814e..1a90d2a2e0 100644 --- a/src/Avalonia.Layout/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -14,6 +14,12 @@ namespace Avalonia.Layout /// public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates { + /// + /// Defines the property. + /// + public static readonly StyledProperty DisableVirtualizationProperty = + AvaloniaProperty.Register(nameof(DisableVirtualization)); + /// /// Defines the property. /// @@ -36,6 +42,15 @@ namespace Avalonia.Layout LayoutId = "StackLayout"; } + /// + /// Gets or sets a value indicating whether virtualization is disabled on the layout. + /// + public bool DisableVirtualization + { + get => GetValue(DisableVirtualizationProperty); + set => SetValue(DisableVirtualizationProperty, value); + } + /// /// Gets or sets the axis along which items are laid out. /// @@ -262,6 +277,8 @@ namespace Avalonia.Layout protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) { + ((StackLayoutState)context.LayoutState).OnMeasureStart(); + var desiredSize = GetFlowAlgorithm(context).Measure( availableSize, context, @@ -270,6 +287,7 @@ namespace Avalonia.Layout Spacing, int.MaxValue, _orientation.ScrollOrientation, + DisableVirtualization, LayoutId); return new Size(desiredSize.Width, desiredSize.Height); @@ -284,8 +302,6 @@ namespace Avalonia.Layout FlowLayoutAlgorithm.LineAlignment.Start, LayoutId); - ((StackLayoutState)context.LayoutState).OnArrangeLayoutEnd(); - return new Size(value.Width, value.Height); } @@ -296,11 +312,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/StackLayoutState.cs b/src/Avalonia.Layout/StackLayoutState.cs index 05ad9bca8e..e6164e02e6 100644 --- a/src/Avalonia.Layout/StackLayoutState.cs +++ b/src/Avalonia.Layout/StackLayoutState.cs @@ -56,6 +56,6 @@ namespace Avalonia.Layout MaxArrangeBounds = Math.Max(MaxArrangeBounds, minorSize); } - internal void OnArrangeLayoutEnd() => MaxArrangeBounds = 0; + internal void OnMeasureStart() => MaxArrangeBounds = 0; } } diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs index ee9cff4a01..1698f61989 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -433,6 +433,7 @@ namespace Avalonia.Layout LineSpacing, _maximumRowsOrColumns, _orientation.ScrollOrientation, + false, LayoutId); // If after Measure the first item is in the realization rect, then we revoke grid state's ownership, @@ -463,45 +464,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.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 03a7b4fceb..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(); } @@ -52,15 +52,15 @@ namespace Avalonia.Native { _parent.WindowStateChanged?.Invoke((WindowState)state); } + + void IAvnWindowEvents.GotInputWhenDisabled() + { + _parent.GotInputWhenDisabled?.Invoke(); + } } public IAvnWindow Native => _native; - public void ShowDialog(IWindowImpl window) - { - _native.ShowDialog(((WindowImpl)window).Native); - } - public void CanResize(bool value) { _native.CanResize = value; @@ -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 }); } @@ -116,5 +116,17 @@ 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) + { + _native.SetParent(((WindowImpl)parent).Native); + } + + public void SetEnabled(bool enable) + { + _native.SetEnabled(enable); + } } } diff --git a/src/Avalonia.Remote.Protocol/DesignMessages.cs b/src/Avalonia.Remote.Protocol/DesignMessages.cs index 5ff16c574d..5c769ad48c 100644 --- a/src/Avalonia.Remote.Protocol/DesignMessages.cs +++ b/src/Avalonia.Remote.Protocol/DesignMessages.cs @@ -1,4 +1,7 @@ using System; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Xml; namespace Avalonia.Remote.Protocol.Designer { @@ -26,6 +29,27 @@ namespace Avalonia.Remote.Protocol.Designer public class ExceptionDetails { + public ExceptionDetails() + { + } + + public ExceptionDetails(Exception e) + { + if (e is TargetInvocationException) + { + e = e.InnerException; + } + + ExceptionType = e.GetType().Name; + Message = e.Message; + + if (e is XmlException xml) + { + LineNumber = xml.LineNumber; + LinePosition = xml.LinePosition; + } + } + public string ExceptionType { get; set; } public string Message { get; set; } public int? LineNumber { get; set; } diff --git a/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs b/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs index 1400bc2ac3..e177993d13 100644 --- a/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs +++ b/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs @@ -24,6 +24,7 @@ namespace Avalonia.Styling private BindingValue _value; private IDisposable? _subscription; private IDisposable? _subscriptionTwoWay; + private IDisposable? _innerSubscription; private bool _isActive; public PropertySetterBindingInstance( @@ -121,6 +122,9 @@ namespace Avalonia.Styling sub.Dispose(); } + _innerSubscription?.Dispose(); + _innerSubscription = null; + base.Dispose(); } @@ -144,13 +148,13 @@ namespace Avalonia.Styling protected override void Subscribed() { - _subscription = _binding.Observable.Subscribe(_inner); + _innerSubscription = _binding.Observable.Subscribe(_inner); } protected override void Unsubscribed() { - _subscription?.Dispose(); - _subscription = null; + _innerSubscription?.Dispose(); + _innerSubscription = null; } private void PublishNext() @@ -160,7 +164,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/BoxShadows.cs b/src/Avalonia.Visuals/Media/BoxShadows.cs index fd187f6409..9e4d6aacb0 100644 --- a/src/Avalonia.Visuals/Media/BoxShadows.cs +++ b/src/Avalonia.Visuals/Media/BoxShadows.cs @@ -21,7 +21,7 @@ namespace Avalonia.Media { _first = shadow; _list = null; - Count = 1; + Count = _first.IsEmpty ? 0 : 1; } public BoxShadows(BoxShadow first, BoxShadow[] rest) @@ -105,8 +105,6 @@ namespace Avalonia.Media return false; } } - - public static implicit operator BoxShadows(BoxShadow shadow) => new BoxShadows(shadow); public bool Equals(BoxShadows other) { diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index 4df26c470d..6fdcd9631b 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -141,13 +141,13 @@ namespace Avalonia.Media /// The radius in the Y dimension of the rounded corners. /// This value will be clamped to the range of 0 to Height/2 /// - /// Box shadow effect parameters + /// Box shadow effect parameters /// /// The brush and the pen can both be null. If the brush is null, then no fill is performed. /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. /// public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0, - BoxShadow boxShadow = default) + BoxShadows boxShadows = default) { if (brush == null && !PenIsVisible(pen)) { @@ -164,7 +164,7 @@ namespace Avalonia.Media radiusY = Math.Min(radiusY, rect.Height / 2); } - PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadow); + PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows); } /// @@ -350,7 +350,7 @@ namespace Avalonia.Media /// /// The matrix /// A disposable used to undo the transformation. - PushedState PushSetTransform(Matrix matrix) + public PushedState PushSetTransform(Matrix matrix) { var oldMatrix = CurrentTransform; CurrentTransform = matrix; 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/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs index 42ee35133e..86e2700c04 100644 --- a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs @@ -11,6 +11,46 @@ 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 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 Bitmap CreateScaledBitmap(PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); + return new Bitmap(factory.ResizeBitmap(PlatformImpl.Item, destinationSize, interpolationMode)); + } + /// /// Initializes a new instance of the class. /// @@ -39,7 +79,7 @@ namespace Avalonia.Media.Imaging { PlatformImpl = impl.Clone(); } - + /// /// Initializes a new instance of the class. /// @@ -48,7 +88,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 a0102a0f33..ba30272b7b 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Platform { @@ -87,17 +89,37 @@ namespace Avalonia.Platform /// /// Loads a bitmap implementation from a file.. /// - /// The filename of the bitmap. + /// The filename of the bitmap. /// An . IBitmapImpl LoadBitmap(string fileName); /// /// Loads a bitmap implementation from a file.. /// - /// The stream to read the bitmap from. + /// The stream to read the bitmap from. /// An . IBitmapImpl LoadBitmap(Stream stream); + /// + /// Loads a bitmap implementation from a stream to a specified width maintaining aspect ratio. + /// + /// 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 stream to a specified height maintaining aspect ratio. + /// + /// 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); + + IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); + /// /// Loads a bitmap implementation from a pixels in memory. /// diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs index b0c4cb62eb..d1110e0613 100644 --- a/src/Avalonia.Visuals/Rect.cs +++ b/src/Avalonia.Visuals/Rect.cs @@ -421,11 +421,50 @@ namespace Avalonia } /// - /// Gets the union of two rectangles. - /// - /// The other rectangle. - /// The union. - public Rect Union(Rect rect) + /// Normalizes the rectangle so both the and are positive, without changing the location of the rectangle + /// + /// Normalized Rect + /// + /// Empty rect will be return when Rect contains invalid values. Like NaN. + /// + public Rect Normalize() + { + Rect rect = this; + + if(double.IsNaN(rect.Right) || double.IsNaN(rect.Bottom) || + double.IsNaN(rect.X) || double.IsNaN(rect.Y) || + double.IsNaN(Height) || double.IsNaN(Width)) + { + return Rect.Empty; + } + + if (rect.Width < 0) + { + var x = X + Width; + var width = X - x; + + rect = rect.WithX(x).WithWidth(width); + } + + if (rect.Height < 0) + { + var y = Y + Height; + var height = Y - y; + + rect = rect.WithY(y).WithHeight(height); + } + + return rect; + } + + + /// + /// Gets the union of two rectangles. + /// + /// The other rectangle. + /// The union. + public Rect Union(Rect rect) { if (IsEmpty) { 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)) { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs index 0b04b97ff2..c49e7705e0 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs @@ -11,7 +11,8 @@ namespace Avalonia.Rendering.SceneGraph { public DrawOperation(Rect bounds, Matrix transform) { - bounds = bounds.TransformToAABB(transform); + bounds = bounds.Normalize().TransformToAABB(transform); + Bounds = new Rect( new Point(Math.Floor(bounds.X), Math.Floor(bounds.Y)), new Point(Math.Ceiling(bounds.Right), Math.Ceiling(bounds.Bottom))); diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 36b72fa28e..bb9a4cf208 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -114,6 +114,9 @@ namespace Avalonia /// public Visual() { + // Disable transitions until we're added to the visual tree. + DisableTransitions(); + var visualChildren = new AvaloniaList(); visualChildren.ResetBehavior = ResetBehavior.Remove; visualChildren.Validate = visual => ValidateVisualChild(visual); @@ -393,6 +396,7 @@ namespace Avalonia RenderTransform.Changed += RenderTransformChanged; } + EnableTransitions(); OnAttachedToVisualTree(e); AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); @@ -429,6 +433,7 @@ namespace Avalonia RenderTransform.Changed -= RenderTransformChanged; } + DisableTransitions(); OnDetachedFromVisualTree(e); DetachedFromVisualTree?.Invoke(this, e); e.Root?.Renderer?.AddDirty(this); diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 1b3d1a7dda..643b037b3f 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -41,9 +41,9 @@ 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; + public object SyncRoot { get; } = new object(); class InputEventContainer @@ -746,7 +746,6 @@ namespace Avalonia.X11 void Cleanup() { - SetTransientParent(null, false); if (_xic != IntPtr.Zero) { XDestroyIC(_xic); @@ -773,38 +772,24 @@ namespace Avalonia.X11 bool ActivateTransientChildIfNeeded() { - 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); + if (_disabled) + { + GotInputWhenDisabled?.Invoke(); + return true; + } + + return false; } - 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); @@ -813,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( @@ -1034,13 +1018,14 @@ namespace Avalonia.X11 { ChangeWMAtoms(value, _x11.Atoms._NET_WM_STATE_ABOVE); } - - public void ShowDialog(IWindowImpl parent) + + public void SetEnabled(bool enable) { - SetTransientParent((X11Window)parent); - ShowCore(); + _disabled = !enable; } + public Action GotInputWhenDisabled { get; set; } + public void SetIcon(IWindowIconImpl icon) { if (icon != null) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 44bbf0590a..26fdb08a4b 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -124,29 +124,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/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index 9222c5ac61..e84c7e34de 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,75 @@ 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); + } + + public ImmutableBitmap(Stream stream, int decodeSize, bool horizontal, BitmapInterpolationMode interpolationMode) + { + 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)); + + // 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 2fee282860..0bc5dd56ac 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -1,13 +1,16 @@ using System; using System.Collections; using System.Collections.Generic; -using System.IO; +using System.IO; +using System.Security.Cryptography; using System.Linq; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; +using Avalonia.Media.Imaging; using Avalonia.OpenGL; using Avalonia.OpenGL.Imaging; using Avalonia.Platform; +using Avalonia.Visuals.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia @@ -57,12 +60,6 @@ namespace Avalonia.Skia return new StreamGeometryImpl(); } - /// - public IBitmapImpl LoadBitmap(Stream stream) - { - return new ImmutableBitmap(stream); - } - /// public IBitmapImpl LoadBitmap(string fileName) { @@ -72,12 +69,43 @@ 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 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/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index 459486f784..6375f74c59 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 96bd96341c..c4c0541d53 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -7,7 +7,9 @@ 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 Avalonia.Visuals.Media.Imaging; using SharpDX.DirectWrite; using GlyphRun = Avalonia.Media.GlyphRun; using TextAlignment = Avalonia.Media.TextAlignment; @@ -179,16 +181,38 @@ 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) + { + return new WicBitmapImpl(stream, width, true, interpolationMode); + } + + /// + public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + return new WicBitmapImpl(stream, height, false, interpolationMode); + } + + /// + public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + // https://github.com/sharpdx/SharpDX/issues/959 blocks implementation. + throw new NotImplementedException(); + } + + /// public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride) { return new WicBitmapImpl(format, data, size, dpi, stride); diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs index f5159b1f84..743abddd1e 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 frame = _decoder.GetFrame(0); + + // now scale that to the size that we want + var realScale = horizontal ? ((double)frame.Size.Height / frame.Size.Width) : ((double)frame.Size.Width / frame.Size.Height); + + PixelSize desired; + + if (horizontal) + { + desired = new PixelSize(decodeSize, (int)(realScale * decodeSize)); + } + else + { + desired = new PixelSize((int)(realScale * decodeSize), decodeSize); + } + + if (frame.Size.Width != desired.Width || frame.Size.Height != desired.Height) + { + using (var scaler = new BitmapScaler(Direct2D1Platform.ImagingFactory)) + { + scaler.Initialize(frame, desired.Width, desired.Height, ConvertInterpolationMode(interpolationMode)); + + WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, scaler, BitmapCreateCacheOption.CacheOnLoad); + } + } + else + { + WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, frame, BitmapCreateCacheOption.CacheOnLoad); + } + + Dpi = new Vector(96, 96); + } + public override Vector Dpi { get; } public override PixelSize PixelSize => WicImpl.Size.ToAvalonia(); 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 diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 608563cb67..cc4c12ec3c 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -45,7 +45,6 @@ namespace Avalonia.Win32 #endif private const WindowStyles WindowStateMask = (WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE); - private readonly List _disabledBy; 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,31 +340,25 @@ namespace Avalonia.Win32 public void Hide() { - if (_parent != null) - { - _parent._disabledBy.Remove(this); - _parent.UpdateEnabled(); - _parent = null; - } - UnmanagedMethods.ShowWindow(_hwnd, ShowWindowCommand.Hide); } 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); + SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, _parent._hwnd); } + public void SetEnabled(bool enable) => EnableWindow(_hwnd, enable); + public void BeginMoveDrag(PointerPressedEventArgs e) { _mouseDevice.Capture(null); @@ -666,7 +658,7 @@ namespace Avalonia.Win32 } } - private WindowStyles GetWindowStateStyles () + private WindowStyles GetWindowStateStyles() { return GetStyle() & WindowStateMask; } @@ -721,11 +713,6 @@ namespace Avalonia.Win32 } } - private void UpdateEnabled() - { - EnableWindow(_hwnd, _disabledBy.Count == 0); - } - private void UpdateWindowProperties(WindowProperties newProperties, bool forceChanges = false) { var oldProperties = _windowProperties; diff --git a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs new file mode 100644 index 0000000000..b5c61883e7 --- /dev/null +++ b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs @@ -0,0 +1,339 @@ +using System; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Layout; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Moq; +using Xunit; + +namespace Avalonia.Animation.UnitTests +{ + public class AnimatableTests + { + [Fact] + public void Transition_Is_Not_Applied_When_Not_Attached_To_Visual_Tree() + { + var target = CreateTarget(); + var control = new Control + { + Transitions = new Transitions { target.Object }, + }; + + control.Opacity = 0.5; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5), + Times.Never); + } + + [Fact] + public void Transition_Is_Not_Applied_To_Initial_Style() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var target = CreateTarget(); + var control = new Control + { + Transitions = new Transitions { target.Object }, + }; + + var root = new TestRoot + { + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(Visual.OpacityProperty, 0.8), + } + } + } + }; + + root.Child = control; + + Assert.Equal(0.8, control.Opacity); + + target.Verify(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + } + + [Fact] + public void Transition_Is_Applied_When_Local_Value_Changes() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + + control.Opacity = 0.5; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5)); + } + + [Fact] + public void Transition_Is_Not_Applied_When_Animated_Value_Changes() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + + control.SetValue(Visual.OpacityProperty, 0.5, BindingPriority.Animation); + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5), + Times.Never); + } + + [Fact] + public void Transition_Is_Not_Applied_When_StyleTrigger_Changes_With_LocalValue_Present() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + + control.SetValue(Visual.OpacityProperty, 0.5); + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5)); + target.ResetCalls(); + + control.SetValue(Visual.OpacityProperty, 0.8, BindingPriority.StyleTrigger); + + target.Verify(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Fact] + public void Transition_Is_Disposed_When_Local_Value_Changes() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + var sub = new Mock(); + + target.Setup(x => x.Apply(control, It.IsAny(), 1.0, 0.5)).Returns(sub.Object); + + control.Opacity = 0.5; + sub.ResetCalls(); + control.Opacity = 0.4; + + sub.Verify(x => x.Dispose()); + } + + [Fact] + public void New_Transition_Is_Applied_When_Local_Value_Changes() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + + target.Setup(x => x.Property).Returns(Visual.OpacityProperty); + target.Setup(x => x.Apply(control, It.IsAny(), 1.0, 0.5)) + .Callback(() => + { + control.SetValue(Visual.OpacityProperty, 0.9, BindingPriority.Animation); + }) + .Returns(Mock.Of()); + + control.Opacity = 0.5; + + Assert.Equal(0.9, control.Opacity); + target.ResetCalls(); + + control.Opacity = 0.4; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 0.9, + 0.4)); + } + + [Fact] + public void Transition_Is_Not_Applied_When_Removed_From_Visual_Tree() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + + control.Opacity = 0.5; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5)); + target.ResetCalls(); + + var root = (TestRoot)control.Parent; + root.Child = null; + control.Opacity = 0.8; + + target.Verify(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Fact] + public void Animation_Is_Cancelled_When_Transition_Removed() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + var sub = new Mock(); + + target.Setup(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).Returns(sub.Object); + + control.Opacity = 0.5; + control.Transitions.RemoveAt(0); + + sub.Verify(x => x.Dispose()); + } + + [Fact] + public void Animation_Is_Cancelled_When_New_Style_Activates() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var target = CreateTarget(); + var control = CreateStyledControl(target.Object); + var sub = new Mock(); + + target.Setup(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5)).Returns(sub.Object); + + control.Opacity = 0.5; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5), + Times.Once); + + control.Classes.Add("foo"); + + sub.Verify(x => x.Dispose()); + } + } + + [Fact] + public void Transition_From_Style_Trigger_Is_Applied() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var target = CreateTransition(Control.WidthProperty); + var control = CreateStyledControl(transition2: target.Object); + var sub = new Mock(); + + control.Classes.Add("foo"); + control.Width = 100; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + double.NaN, + 100.0), + Times.Once); + } + } + + private static Mock CreateTarget() + { + return CreateTransition(Visual.OpacityProperty); + } + + private static Control CreateControl(ITransition transition) + { + var control = new Control + { + Transitions = new Transitions { transition }, + }; + + var root = new TestRoot(control); + return control; + } + + private static Control CreateStyledControl( + ITransition transition1 = null, + ITransition transition2 = null) + { + transition1 = transition1 ?? CreateTarget().Object; + transition2 = transition2 ?? CreateTransition(Control.WidthProperty).Object; + + var control = new Control + { + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter + { + Property = Control.TransitionsProperty, + Value = new Transitions { transition1 }, + } + } + }, + new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter + { + Property = Control.TransitionsProperty, + Value = new Transitions { transition2 }, + } + } + } + } + }; + + var root = new TestRoot(control); + return control; + } + + private static Mock CreateTransition(AvaloniaProperty property) + { + var target = new Mock(); + var sub = new Mock(); + + target.Setup(x => x.Property).Returns(property); + target.Setup(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).Returns(sub.Object); + + return target; + } + } +} diff --git a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs index 22f3b4f501..70ffd781a1 100644 --- a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs +++ b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs @@ -16,7 +16,7 @@ namespace Avalonia.Animation.UnitTests { var border = new Border { - Transitions = + Transitions = new Transitions { new DoubleTransition { @@ -44,7 +44,7 @@ namespace Avalonia.Animation.UnitTests { var border = new Border { - Transitions = + Transitions = new Transitions { new DoubleTransition { 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..d7f927372e 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,30 @@ 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 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() { @@ -144,6 +169,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(); @@ -161,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 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.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 5e29893946..d9fb5e03fc 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.Controls.UnitTests/Shapes/PathTests.cs b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs index 5a9ca410e4..88c64e76cc 100644 --- a/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs @@ -23,12 +23,16 @@ namespace Avalonia.Controls.UnitTests.Shapes var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) }; var target = new Path { Data = geometry }; + var root = new TestRoot(target); + target.Measure(Size.Infinity); Assert.True(target.IsMeasureValid); geometry.Rect = new Rect(0, 0, 20, 20); Assert.False(target.IsMeasureValid); + + root.Child = null; } } } 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), diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 0afb2465ee..9bb9fd7145 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.Remoting.Contexts; using Avalonia.Controls; +using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Diagnostics; using Avalonia.Input; @@ -492,6 +493,45 @@ namespace Avalonia.LeakTests } } + [Fact] + public void Path_Is_Freed() + { + using (Start()) + { + var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) }; + + Func run = () => + { + var window = new Window + { + Content = new Path + { + Data = geometry + } + }; + + window.Show(); + + window.LayoutManager.ExecuteInitialLayoutPass(window); + Assert.IsType(window.Presenter.Child); + + window.Content = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Null(window.Presenter.Child); + + return window; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + + // We are keeping geometry alive to simulate a resource that outlives the control. + GC.KeepAlive(geometry); + } + } + private IDisposable Start() { return UnitTestApplication.Start(TestServices.StyledWindow.With( 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.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 02f0d7072c..9642f5719d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -337,5 +337,50 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(Brushes.Red, listBox.Background); } } + + [Fact] + public void Transitions_Can_Be_Styled() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var border = (Border)window.Content; + + Assert.Equal(1, border.Transitions.Count); + Assert.Equal(Border.WidthProperty, border.Transitions[0].Property); + + border.Classes.Add("foo"); + + Assert.Equal(1, border.Transitions.Count); + Assert.Equal(Border.HeightProperty, border.Transitions[0].Property); + + border.Classes.Remove("foo"); + + Assert.Equal(1, border.Transitions.Count); + Assert.Equal(Border.WidthProperty, border.Transitions[0].Property); + } + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index 4ff9e3db38..a408069cb0 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -34,9 +34,11 @@ namespace Avalonia.Markup.Xaml.UnitTests var parsed = (Grid)AvaloniaXamlLoader.Parse(@" - + + + "); Assert.Equal(1, parsed.Transitions.Count); diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index afdf95430b..5f2c6d1a69 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, diff --git a/tests/Avalonia.Visuals.UnitTests/RectTests.cs b/tests/Avalonia.Visuals.UnitTests/RectTests.cs index 34953e5fd7..a2b0569949 100644 --- a/tests/Avalonia.Visuals.UnitTests/RectTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/RectTests.cs @@ -35,5 +35,24 @@ namespace Avalonia.Visuals.UnitTests Assert.Equal(new Rect(0, 0, 100, 100), result); } + + [Fact] + public void Normalize_Should_Reverse_Negative_Size() + { + var result = new Rect(new Point(100, 100), new Point(0, 0)).Normalize(); + + Assert.Equal(new Rect(0, 0, 100, 100), result); + } + + [Fact] + public void Normalize_Should_Make_Invalid_Rects_Empty() + { + var result = new Rect( + double.NegativeInfinity, double.PositiveInfinity, + double.PositiveInfinity, double.PositiveInfinity) + .Normalize(); + + Assert.Equal(Rect.Empty, result); + } } } 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)) diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 019eefe1a6..558a9a5968 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 { @@ -83,6 +84,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();