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..b86c679397 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -258,6 +258,7 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0; virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) = 0; + virtual HRESULT SetBlurEnabled (bool enable) = 0; }; AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase @@ -267,7 +268,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 +311,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..bdf3007a28 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -3,6 +3,9 @@ class WindowBaseImpl; +@interface AutoFitContentVisualEffectView : NSVisualEffectView +@end + @interface AvnView : NSView -(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(NSEvent* _Nonnull) lastMouseDownEvent; @@ -19,8 +22,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..abfae3cf1e 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -20,6 +20,7 @@ public: View = NULL; Window = NULL; } + NSVisualEffectView* VisualEffect; AvnView* View; AvnWindow* Window; ComPtr BaseEvents; @@ -47,6 +48,12 @@ public: [Window setStyleMask:NSWindowStyleMaskBorderless]; [Window setBackingType:NSBackingStoreBuffered]; + + VisualEffect = [AutoFitContentVisualEffectView new]; + [VisualEffect setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; + [VisualEffect setMaterial:NSVisualEffectMaterialLight]; + [VisualEffect setAutoresizesSubviews:true]; + [Window setContentView: View]; } @@ -383,6 +390,18 @@ public: return *ppv == nil ? E_FAIL : S_OK; } + virtual HRESULT SetBlurEnabled (bool enable) override + { + [Window setContentView: enable ? VisualEffect : View]; + + if(enable) + { + [VisualEffect addSubview:View]; + } + + return S_OK; + } + virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) override @@ -497,12 +516,7 @@ private: virtual HRESULT Show () override { @autoreleasepool - { - if([Window parentWindow] != nil) - [[Window parentWindow] removeChildWindow:Window]; - - [Window setModal:FALSE]; - + { WindowBaseImpl::Show(); HideOrShowTrafficLights(); @@ -511,7 +525,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 +545,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 +903,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,12 +920,26 @@ protected: break; } + if([Window parentWindow] == nullptr) + { + s |= NSWindowStyleMaskMiniaturizable; + } return s; } }; NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil]; +@implementation AutoFitContentVisualEffectView +-(void)setFrameSize:(NSSize)newSize +{ + [super setFrameSize:newSize]; + if([[self subviews] count] == 0) + return; + [[self subviews][0] setFrameSize: newSize]; +} +@end + @implementation AvnView { ComPtr _parent; @@ -1081,15 +1115,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 +1281,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 +1466,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent ComPtr _parent; bool _canBecomeKeyAndMain; bool _closed; - bool _isModal; + bool _isEnabled; AvnMenu* _menu; double _lastScaling; } @@ -1538,6 +1588,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _parent = parent; [self setDelegate:self]; _closed = false; + _isEnabled = true; _lastScaling = [self backingScaleFactor]; [self setOpaque:NO]; @@ -1604,28 +1655,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/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 30671ef083..add5dbde84 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -2,7 +2,7 @@ xmlns:pages="clr-namespace:ControlCatalog.Pages" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.MainView" - Background="{DynamicResource ThemeBackgroundBrush}" + Background="Transparent" Foreground="{DynamicResource ThemeForegroundBrush}" FontSize="{DynamicResource FontSizeNormal}"> @@ -70,6 +70,12 @@ Light Dark + + None + Transparent + Blur + AcrylicBlur + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 7c17b125d6..8c4818fb07 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -63,6 +63,13 @@ namespace ControlCatalog if (VisualRoot is Window window) window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex; }; + + var transparencyLevels = this.Find("TransparencyLevels"); + transparencyLevels.SelectionChanged += (sender, e) => + { + if (VisualRoot is Window window) + window.TransparencyLevelHint = (WindowTransparencyLevel)transparencyLevels.SelectedIndex; + }; } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 935db20757..a0bb956425 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -7,8 +7,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:v="clr-namespace:ControlCatalog.Views" - x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}"> - + x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" Background="Transparent"> diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props index b9075b957b..8fc91aca14 100644 --- a/samples/Directory.Build.props +++ b/samples/Directory.Build.props @@ -1,6 +1,7 @@ false + $(MSBuildThisFileDirectory)..\src\tools\Avalonia.Designer.HostApp\bin\Debug\netcoreapp2.0\Avalonia.Designer.HostApp.dll diff --git a/samples/RenderDemo/Controls/LineBoundsDemoControl.cs b/samples/RenderDemo/Controls/LineBoundsDemoControl.cs index 0e0b3d6142..cc847a594d 100644 --- a/samples/RenderDemo/Controls/LineBoundsDemoControl.cs +++ b/samples/RenderDemo/Controls/LineBoundsDemoControl.cs @@ -17,7 +17,7 @@ namespace RenderDemo.Controls public LineBoundsDemoControl() { var timer = new DispatcherTimer(); - timer.Interval = TimeSpan.FromSeconds(1 / 60); + timer.Interval = TimeSpan.FromSeconds(1 / 60.0); timer.Tick += (sender, e) => Angle += Math.PI / 360; timer.Start(); } 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/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 66b4676b45..d0d88166a7 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -115,45 +115,46 @@ namespace Avalonia.Utilities return true; } + var toUnderl = Nullable.GetUnderlyingType(to) ?? to; var from = value.GetType(); - if (to.IsAssignableFrom(from)) + if (toUnderl.IsAssignableFrom(from)) { result = value; return true; } - if (to == typeof(string)) + if (toUnderl == typeof(string)) { - result = Convert.ToString(value); + result = Convert.ToString(value, culture); return true; } - if (to.IsEnum && from == typeof(string)) + if (toUnderl.IsEnum && from == typeof(string)) { - if (Enum.IsDefined(to, (string)value)) + if (Enum.IsDefined(toUnderl, (string)value)) { - result = Enum.Parse(to, (string)value); + result = Enum.Parse(toUnderl, (string)value); return true; } } - if (!from.IsEnum && to.IsEnum) + if (!from.IsEnum && toUnderl.IsEnum) { result = null; - if (TryConvert(Enum.GetUnderlyingType(to), value, culture, out object enumValue)) + if (TryConvert(Enum.GetUnderlyingType(toUnderl), value, culture, out object enumValue)) { - result = Enum.ToObject(to, enumValue); + result = Enum.ToObject(toUnderl, enumValue); return true; } } - if (from.IsEnum && IsNumeric(to)) + if (from.IsEnum && IsNumeric(toUnderl)) { try { - result = Convert.ChangeType((int)value, to, culture); + result = Convert.ChangeType((int)value, toUnderl, culture); return true; } catch @@ -164,7 +165,7 @@ namespace Avalonia.Utilities } var convertableFrom = Array.IndexOf(InbuiltTypes, from); - var convertableTo = Array.IndexOf(InbuiltTypes, to); + var convertableTo = Array.IndexOf(InbuiltTypes, toUnderl); if (convertableFrom != -1 && convertableTo != -1) { @@ -172,7 +173,7 @@ namespace Avalonia.Utilities { try { - result = Convert.ChangeType(value, to, culture); + result = Convert.ChangeType(value, toUnderl, culture); return true; } catch @@ -183,15 +184,23 @@ namespace Avalonia.Utilities } } - var typeConverter = TypeDescriptor.GetConverter(to); + var toTypeConverter = TypeDescriptor.GetConverter(toUnderl); + + if (toTypeConverter.CanConvertFrom(from) == true) + { + result = toTypeConverter.ConvertFrom(null, culture, value); + return true; + } + + var fromTypeConverter = TypeDescriptor.GetConverter(from); - if (typeConverter.CanConvertFrom(from) == true) + if (fromTypeConverter.CanConvertTo(toUnderl) == true) { - result = typeConverter.ConvertFrom(null, culture, value); + result = fromTypeConverter.ConvertTo(null, culture, value, toUnderl); return true; } - var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit | OperatorType.Explicit); + var cast = FindTypeConversionOperatorMethod(from, toUnderl, OperatorType.Implicit | OperatorType.Explicit); if (cast != null) { 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/Application.cs b/src/Avalonia.Controls/Application.cs index 02f47e07b4..3bf72460df 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -30,7 +30,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceNode + public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost { /// /// The application-global data templates. @@ -129,26 +129,13 @@ namespace Avalonia /// public IResourceDictionary Resources { - get => _resources ?? (Resources = new ResourceDictionary()); + get => _resources ??= new ResourceDictionary(this); set { - Contract.Requires(value != null); - - var hadResources = false; - - if (_resources != null) - { - hadResources = _resources.Count > 0; - _resources.ResourcesChanged -= ThisResourcesChanged; - } - + value = value ?? throw new ArgumentNullException(nameof(value)); + _resources?.RemoveOwner(this); _resources = value; - _resources.ResourcesChanged += ThisResourcesChanged; - - if (hadResources || _resources.Count > 0) - { - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - } + _resources.AddOwner(this); } } @@ -161,23 +148,15 @@ namespace Avalonia /// /// Global styles apply to all windows in the application. /// - public Styles Styles - { - get - { - if (_styles == null) - { - _styles = new Styles(this); - _styles.ResourcesChanged += ThisResourcesChanged; - } - - return _styles; - } - } + public Styles Styles => _styles ??= new Styles(this); /// bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; + /// + bool IResourceNode.HasResources => (_resources?.HasResources ?? false) || + (((IResourceNode?)_styles)?.HasResources ?? false); + /// /// Gets the styling parent of the application, which is null. /// @@ -185,13 +164,7 @@ namespace Avalonia /// bool IStyleHost.IsStylesInitialized => _styles != null; - - /// - bool IResourceProvider.HasResources => _resources?.Count > 0; - - /// - IResourceNode IResourceNode.ResourceParent => null; - + /// /// Application lifetime, use it for things like setting the main window and exiting the app from code /// Currently supported lifetimes are: @@ -219,13 +192,18 @@ namespace Avalonia public virtual void Initialize() { } /// - bool IResourceProvider.TryGetResource(object key, out object value) + bool IResourceNode.TryGetResource(object key, out object value) { value = null; return (_resources?.TryGetResource(key, out value) ?? false) || Styles.TryGetResource(key, out value); } + void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e) + { + ResourcesChanged?.Invoke(this, e); + } + void IStyleHost.StylesAdded(IReadOnlyList styles) { _stylesAdded?.Invoke(styles); @@ -282,9 +260,7 @@ namespace Avalonia try { _notifyingResourcesChanged = true; - (_resources as ISetResourceParent)?.ParentResourcesChanged(e); - (_styles as ISetResourceParent)?.ParentResourcesChanged(e); - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + ResourcesChanged?.Invoke(this, ResourcesChangedEventArgs.Empty); } finally { diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 3e4f47ec8a..b38cc56a17 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -10,6 +10,7 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; +using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Avalonia.Collections; @@ -1100,6 +1101,7 @@ namespace Avalonia.Controls { _textBoxSubscriptions = _textBox.GetObservable(TextBox.TextProperty) + .Skip(1) .Subscribe(_ => OnTextBoxTextChanged()); if (Text != null) 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/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 1735599988..86499530da 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Linq; using Avalonia.Controls.Generators; @@ -9,18 +10,19 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.LogicalTree; +using Avalonia.Styling; namespace Avalonia.Controls { /// /// A control context menu. /// - public class ContextMenu : MenuBase + public class ContextMenu : MenuBase, ISetterValue { private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel { Orientation = Orientation.Vertical }); private Popup _popup; - private Control _attachedControl; + private List _attachedControls; private IInputElement _previousFocus; /// @@ -74,13 +76,14 @@ namespace Avalonia.Controls if (e.OldValue is ContextMenu oldMenu) { control.PointerReleased -= ControlPointerReleased; - oldMenu._attachedControl = null; + oldMenu._attachedControls?.Remove(control); ((ISetLogicalParent)oldMenu._popup)?.SetParent(null); } if (e.NewValue is ContextMenu newMenu) { - newMenu._attachedControl = control; + newMenu._attachedControls ??= new List(); + newMenu._attachedControls.Add(control); control.PointerReleased += ControlPointerReleased; } } @@ -96,18 +99,22 @@ namespace Avalonia.Controls /// The control. public void Open(Control control) { - if (control is null && _attachedControl is null) + if (control is null && (_attachedControls is null || _attachedControls.Count == 0)) { throw new ArgumentNullException(nameof(control)); } - if (control is object && _attachedControl is object && control != _attachedControl) + if (control is object && + _attachedControls is object && + !_attachedControls.Contains(control)) { throw new ArgumentException( "Cannot show ContentMenu on a different control to the one it is attached to.", nameof(control)); } + control ??= _attachedControls[0]; + if (IsOpen) { return; @@ -126,7 +133,12 @@ namespace Avalonia.Controls _popup.Closed += PopupClosed; } - ((ISetLogicalParent)_popup).SetParent(control); + if (_popup.Parent != control) + { + ((ISetLogicalParent)_popup).SetParent(null); + ((ISetLogicalParent)_popup).SetParent(control); + } + _popup.Child = this; _popup.IsOpen = true; @@ -155,6 +167,17 @@ namespace Avalonia.Controls } } + void ISetterValue.Initialize(ISetter setter) + { + // ContextMenu can be assigned to the ContextMenu property in a setter. This overrides + // the behavior defined in Control which requires controls to be wrapped in a