From 02e30f80f5c2293e115ef8b0679e4ddd8537d166 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Wed, 8 Sep 2021 20:53:05 +0300 Subject: [PATCH 001/134] [OSX] Add Swipe, Rotate and Magnify PitchToZoom trackpad gestures as native mouse events, because they can be triggered from magic mouse AFAIK and creation of new TrackpadDevice is too big task for me now --- native/Avalonia.Native/src/OSX/window.mm | 31 ++++++++++++++++++++++++ src/Avalonia.Native/avn.idl | 5 +++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 14fe60ab0b..b1e923c434 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1625,6 +1625,19 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return; } } + else if (type == Magnify) + { + delta.X = delta.Y = [event magnification]; + } + else if (type == Rotate) + { + delta.X = delta.Y = [event rotation]; + } + else if (type == Swipe) + { + delta.X = [event deltaX]; + delta.Y = [event deltaY]; + } auto timestamp = [event timestamp] * 1000; auto modifiers = [self getModifiers:[event modifierFlags]]; @@ -1749,6 +1762,24 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [super scrollWheel:event]; } +- (void)magnifyWithEvent:(NSEvent *)event +{ + [self mouseEvent:event withType:Magnify]; + [super magnifyWithEvent:event]; +} + +- (void)rotateWithEvent:(NSEvent *)event +{ + [self mouseEvent:event withType:Rotate]; + [super rotateWithEvent:event]; +} + +- (void)swipeWithEvent:(NSEvent *)event +{ + [self mouseEvent:event withType:Swipe]; + [super swipeWithEvent:event]; +} + - (void)mouseEntered:(NSEvent *)event { _isMouseOver = true; diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 70d85dacdd..05fcdc71a5 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -295,7 +295,10 @@ enum AvnRawMouseEventType TouchBegin, TouchUpdate, TouchEnd, - TouchCancel + TouchCancel, + Magnify, + Rotate, + Swipe, } enum AvnRawKeyEventType From 33c22a952f13b7217cce887c1f4dfd739f98cce4 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Wed, 8 Sep 2021 21:51:50 +0300 Subject: [PATCH 002/134] [OSX] Add PointerMagnifyGesture, PointerRotateGesture, PointerSwipeGesture events --- src/Avalonia.Input/InputElement.cs | 83 ++++++++++++++++++- src/Avalonia.Input/MouseDevice.cs | 72 ++++++++++++++++ .../PointerMagnifyGestureEventArgs.cs | 19 +++++ .../PointerRotateGestureEventArgs.cs | 19 +++++ .../PointerSwipeGestureEventArgs.cs | 19 +++++ src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 5 +- .../Raw/RawPointerGestureEventArgs.cs | 19 +++++ src/Avalonia.Native/WindowImplBase.cs | 21 ++++- 8 files changed, 253 insertions(+), 4 deletions(-) create mode 100644 src/Avalonia.Input/PointerMagnifyGestureEventArgs.cs create mode 100644 src/Avalonia.Input/PointerRotateGestureEventArgs.cs create mode 100644 src/Avalonia.Input/PointerSwipeGestureEventArgs.cs create mode 100644 src/Avalonia.Input/Raw/RawPointerGestureEventArgs.cs diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 63080e74e4..93434af4a1 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -186,6 +186,30 @@ namespace Avalonia.Input RoutedEvent.Register( "PointerWheelChanged", RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerMagnifyGestureEvent = + RoutedEvent.Register( + "PointerMagnifyGesture", + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerRotateGestureEvent = + RoutedEvent.Register( + "PointerRotateGesture", + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerSwipeGestureEvent = + RoutedEvent.Register( + "PointerSwipeGesture", + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); /// /// Defines the event. @@ -223,6 +247,9 @@ namespace Avalonia.Input PointerReleasedEvent.AddClassHandler((x, e) => x.OnPointerReleased(e)); PointerCaptureLostEvent.AddClassHandler((x, e) => x.OnPointerCaptureLost(e)); PointerWheelChangedEvent.AddClassHandler((x, e) => x.OnPointerWheelChanged(e)); + PointerMagnifyGestureEvent.AddClassHandler((x, e) => x.OnPointerMagnifyGesture(e)); + PointerRotateGestureEvent.AddClassHandler((x, e) => x.OnPointerRotateGesture(e)); + PointerSwipeGestureEvent.AddClassHandler((x, e) => x.OnPointerSwipeGesture(e)); } public InputElement() @@ -349,13 +376,43 @@ namespace Avalonia.Input } /// - /// Occurs when the mouse wheen is scrolled over the control. + /// Occurs when the mouse is scrolled over the control. /// public event EventHandler PointerWheelChanged { add { AddHandler(PointerWheelChangedEvent, value); } remove { RemoveHandler(PointerWheelChangedEvent, value); } } + + /// + /// Occurs when the user uses magnify (Pitch to Zoom) gesture on a trackpad and pointer is over the control. + /// Works only on macOS. + /// + public event EventHandler PointerMagnifyGesture + { + add { AddHandler(PointerMagnifyGestureEvent, value); } + remove { RemoveHandler(PointerMagnifyGestureEvent, value); } + } + + /// + /// Occurs when the user uses rotate gesture on a trackpad and pointer is over the control. + /// Works only on macOS. + /// + public event EventHandler PointerRotateGesture + { + add { AddHandler(PointerRotateGestureEvent, value); } + remove { RemoveHandler(PointerRotateGestureEvent, value); } + } + + /// + /// Occurs when the user uses swipe gesture on a trackpad and pointer is over the control. + /// Works only on macOS. + /// + public event EventHandler PointerSwipeGesture + { + add { AddHandler(PointerSwipeGestureEvent, value); } + remove { RemoveHandler(PointerSwipeGestureEvent, value); } + } /// /// Occurs when a tap gesture occurs on the control. @@ -617,6 +674,30 @@ namespace Avalonia.Input protected virtual void OnPointerWheelChanged(PointerWheelEventArgs e) { } + + /// + /// Called before the trackpad event occurs. + /// + /// The event args. + protected virtual void OnPointerMagnifyGesture(PointerMagnifyGestureEventArgs e) + { + } + + /// + /// Called before the trackpad event occurs. + /// + /// The event args. + protected virtual void OnPointerRotateGesture(PointerRotateGestureEventArgs e) + { + } + + /// + /// Called before the trackpad event occurs. + /// + /// The event args. + protected virtual void OnPointerSwipeGesture(PointerSwipeGestureEventArgs e) + { + } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index cfa3690daf..745c0caafd 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -178,6 +178,15 @@ namespace Avalonia.Input case RawPointerEventType.Wheel: e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); break; + case RawPointerEventType.Magnify: + e.Handled = GestureMagnify(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); + break; + case RawPointerEventType.Rotate: + e.Handled = GestureRotate(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); + break; + case RawPointerEventType.Swipe: + e.Handled = GestureSwipe(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); + break; } } @@ -331,6 +340,69 @@ namespace Avalonia.Input return false; } + + private bool GestureMagnify(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties props, + Vector delta, KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var hit = HitTest(root, p); + + if (hit != null) + { + var source = GetSource(hit); + var e = new PointerMagnifyGestureEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta.X); + + source?.RaiseEvent(e); + return e.Handled; + } + + return false; + } + + private bool GestureRotate(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties props, + Vector delta, KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var hit = HitTest(root, p); + + if (hit != null) + { + var source = GetSource(hit); + var e = new PointerRotateGestureEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta.X); + + source?.RaiseEvent(e); + return e.Handled; + } + + return false; + } + + private bool GestureSwipe(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties props, + Vector delta, KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var hit = HitTest(root, p); + + if (hit != null) + { + var source = GetSource(hit); + var e = new PointerSwipeGestureEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); + + source?.RaiseEvent(e); + return e.Handled; + } + + return false; + } private IInteractive GetSource(IVisual hit) { diff --git a/src/Avalonia.Input/PointerMagnifyGestureEventArgs.cs b/src/Avalonia.Input/PointerMagnifyGestureEventArgs.cs new file mode 100644 index 0000000000..828307c5a7 --- /dev/null +++ b/src/Avalonia.Input/PointerMagnifyGestureEventArgs.cs @@ -0,0 +1,19 @@ +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + public class PointerMagnifyGestureEventArgs : PointerEventArgs + { + public double Delta { get; set; } + + public PointerMagnifyGestureEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, + Point rootVisualPosition, ulong timestamp, + PointerPointProperties properties, KeyModifiers modifiers, double delta) + : base(InputElement.PointerMagnifyGestureEvent, source, pointer, rootVisual, rootVisualPosition, + timestamp, properties, modifiers) + { + Delta = delta; + } + } +} diff --git a/src/Avalonia.Input/PointerRotateGestureEventArgs.cs b/src/Avalonia.Input/PointerRotateGestureEventArgs.cs new file mode 100644 index 0000000000..4bf1ffaaaa --- /dev/null +++ b/src/Avalonia.Input/PointerRotateGestureEventArgs.cs @@ -0,0 +1,19 @@ +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + public class PointerRotateGestureEventArgs : PointerEventArgs + { + public double Delta { get; set; } + + public PointerRotateGestureEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, + Point rootVisualPosition, ulong timestamp, + PointerPointProperties properties, KeyModifiers modifiers, double delta) + : base(InputElement.PointerRotateGestureEvent, source, pointer, rootVisual, rootVisualPosition, + timestamp, properties, modifiers) + { + Delta = delta; + } + } +} diff --git a/src/Avalonia.Input/PointerSwipeGestureEventArgs.cs b/src/Avalonia.Input/PointerSwipeGestureEventArgs.cs new file mode 100644 index 0000000000..c4994c8504 --- /dev/null +++ b/src/Avalonia.Input/PointerSwipeGestureEventArgs.cs @@ -0,0 +1,19 @@ +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + public class PointerSwipeGestureEventArgs : PointerEventArgs + { + public Vector Delta { get; set; } + + public PointerSwipeGestureEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, + Point rootVisualPosition, ulong timestamp, + PointerPointProperties properties, KeyModifiers modifiers, Vector delta) + : base(InputElement.PointerSwipeGestureEvent, source, pointer, rootVisual, rootVisualPosition, + timestamp, properties, modifiers) + { + Delta = delta; + } + } +} diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 62a1dd5d84..d6406121c7 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -21,7 +21,10 @@ namespace Avalonia.Input.Raw TouchBegin, TouchUpdate, TouchEnd, - TouchCancel + TouchCancel, + Magnify, + Rotate, + Swipe } /// diff --git a/src/Avalonia.Input/Raw/RawPointerGestureEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerGestureEventArgs.cs new file mode 100644 index 0000000000..5a6dbda9a7 --- /dev/null +++ b/src/Avalonia.Input/Raw/RawPointerGestureEventArgs.cs @@ -0,0 +1,19 @@ +namespace Avalonia.Input.Raw +{ + public class RawPointerGestureEventArgs : RawPointerEventArgs + { + public RawPointerGestureEventArgs( + IInputDevice device, + ulong timestamp, + IInputRoot root, + RawPointerEventType gestureType, + Point position, + Vector delta, RawInputModifiers inputModifiers) + : base(device, timestamp, root, gestureType, position, inputModifiers) + { + Delta = delta; + } + + public Vector Delta { get; private set; } + } +} diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 4a3baa2788..f1c42efb7f 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -306,11 +306,28 @@ namespace Avalonia.Native switch (type) { case AvnRawMouseEventType.Wheel: - Input?.Invoke(new RawMouseWheelEventArgs(_mouse, timeStamp, _inputRoot, point.ToAvaloniaPoint(), new Vector(delta.X, delta.Y), (RawInputModifiers)modifiers)); + Input?.Invoke(new RawMouseWheelEventArgs(_mouse, timeStamp, _inputRoot, + point.ToAvaloniaPoint(), new Vector(delta.X, delta.Y), (RawInputModifiers)modifiers)); + break; + + case AvnRawMouseEventType.Magnify: + Input?.Invoke(new RawPointerGestureEventArgs(_mouse, timeStamp, _inputRoot, RawPointerEventType.Magnify, + point.ToAvaloniaPoint(), new Vector(delta.X, delta.Y), (RawInputModifiers)modifiers)); + break; + + case AvnRawMouseEventType.Rotate: + Input?.Invoke(new RawPointerGestureEventArgs(_mouse, timeStamp, _inputRoot, RawPointerEventType.Rotate, + point.ToAvaloniaPoint(), new Vector(delta.X, delta.Y), (RawInputModifiers)modifiers)); + break; + + case AvnRawMouseEventType.Swipe: + Input?.Invoke(new RawPointerGestureEventArgs(_mouse, timeStamp, _inputRoot, RawPointerEventType.Swipe, + point.ToAvaloniaPoint(), new Vector(delta.X, delta.Y), (RawInputModifiers)modifiers)); break; default: - var e = new RawPointerEventArgs(_mouse, timeStamp, _inputRoot, (RawPointerEventType)type, point.ToAvaloniaPoint(), (RawInputModifiers)modifiers); + var e = new RawPointerEventArgs(_mouse, timeStamp, _inputRoot, (RawPointerEventType)type, + point.ToAvaloniaPoint(), (RawInputModifiers)modifiers); if(!ChromeHitTest(e)) { From ae48a9a13b22df8bd2cecfff923e9a5374f31cdd Mon Sep 17 00:00:00 2001 From: Foaltin Dorin Date: Fri, 12 Nov 2021 15:37:59 +0200 Subject: [PATCH 003/134] Fix DataGrid selection broken after 3 right clicks --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 10c7c16488..95ee73be4e 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -5751,6 +5751,7 @@ namespace Avalonia.Controls return true; } // Unselect everything except the row that was clicked on + _noSelectionChangeCount++; try { UpdateSelectionAndCurrency(columnIndex, slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false); From 17ec020239ca4b737c60195b0e039ea98f7b7ca1 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Sat, 27 Nov 2021 16:51:04 +0300 Subject: [PATCH 004/134] [OSX] Trackpad Gestures - code refactoring --- src/Avalonia.Input/Gestures.cs | 12 +++ src/Avalonia.Input/InputElement.cs | 85 ++++++------------- src/Avalonia.Input/MouseDevice.cs | 6 +- ...PointerTouchPadGestureMagnifyEventArgs.cs} | 6 +- ... PointerTouchPadGestureRotateEventArgs.cs} | 6 +- ...> PointerTouchPadGestureSwipeEventArgs.cs} | 6 +- 6 files changed, 50 insertions(+), 71 deletions(-) rename src/Avalonia.Input/{PointerRotateGestureEventArgs.cs => PointerTouchPadGestureMagnifyEventArgs.cs} (55%) rename src/Avalonia.Input/{PointerMagnifyGestureEventArgs.cs => PointerTouchPadGestureRotateEventArgs.cs} (55%) rename src/Avalonia.Input/{PointerSwipeGestureEventArgs.cs => PointerTouchPadGestureSwipeEventArgs.cs} (55%) diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 639b4ef117..f218bcb9ce 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -28,6 +28,18 @@ namespace Avalonia.Input public static readonly RoutedEvent ScrollGestureEndedEvent = RoutedEvent.Register( "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); + + public static readonly RoutedEvent PointerTouchPadGestureMagnifyEvent = + RoutedEvent.Register( + "PointerMagnifyGesture", RoutingStrategies.Bubble, typeof(Gestures)); + + public static readonly RoutedEvent PointerTouchPadGestureRotateEvent = + RoutedEvent.Register( + "PointerRotateGesture", RoutingStrategies.Bubble, typeof(Gestures)); + + public static readonly RoutedEvent PointerTouchPadGestureSwipeEvent = + RoutedEvent.Register( + "PointerSwipeGesture", RoutingStrategies.Bubble, typeof(Gestures)); #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. private static readonly WeakReference s_lastPress = new WeakReference(null); diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 88a5b0e971..af135e3e9f 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -186,40 +186,34 @@ namespace Avalonia.Input RoutedEvent.Register( "PointerWheelChanged", RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - + /// - /// Defines the event. + /// Defines the event. /// - public static readonly RoutedEvent PointerMagnifyGestureEvent = - RoutedEvent.Register( - "PointerMagnifyGesture", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - + public static readonly RoutedEvent TappedEvent = Gestures.TappedEvent; + /// - /// Defines the event. + /// Defines the event. /// - public static readonly RoutedEvent PointerRotateGestureEvent = - RoutedEvent.Register( - "PointerRotateGesture", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent; /// - /// Defines the event. + /// Defines the event. /// - public static readonly RoutedEvent PointerSwipeGestureEvent = - RoutedEvent.Register( - "PointerSwipeGesture", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - + public static readonly RoutedEvent PointerTouchPadGestureMagnifyEvent + = Gestures.PointerTouchPadGestureMagnifyEvent; + /// - /// Defines the event. + /// Defines the event. /// - public static readonly RoutedEvent TappedEvent = Gestures.TappedEvent; - + public static readonly RoutedEvent PointerTouchPadGestureRotateEvent + = Gestures.PointerTouchPadGestureRotateEvent; + /// - /// Defines the event. + /// Defines the event. /// - public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent; + public static readonly RoutedEvent PointerTouchPadGestureSwipeEvent + = Gestures.PointerTouchPadGestureSwipeEvent; private bool _isEffectivelyEnabled = true; private bool _isFocused; @@ -247,9 +241,6 @@ namespace Avalonia.Input PointerReleasedEvent.AddClassHandler((x, e) => x.OnPointerReleased(e)); PointerCaptureLostEvent.AddClassHandler((x, e) => x.OnPointerCaptureLost(e)); PointerWheelChangedEvent.AddClassHandler((x, e) => x.OnPointerWheelChanged(e)); - PointerMagnifyGestureEvent.AddClassHandler((x, e) => x.OnPointerMagnifyGesture(e)); - PointerRotateGestureEvent.AddClassHandler((x, e) => x.OnPointerRotateGesture(e)); - PointerSwipeGestureEvent.AddClassHandler((x, e) => x.OnPointerSwipeGesture(e)); } public InputElement() @@ -388,30 +379,30 @@ namespace Avalonia.Input /// Occurs when the user uses magnify (Pitch to Zoom) gesture on a trackpad and pointer is over the control. /// Works only on macOS. /// - public event EventHandler PointerMagnifyGesture + public event EventHandler PointerTouchPadGestureMagnify { - add { AddHandler(PointerMagnifyGestureEvent, value); } - remove { RemoveHandler(PointerMagnifyGestureEvent, value); } + add { AddHandler(PointerTouchPadGestureMagnifyEvent, value); } + remove { RemoveHandler(PointerTouchPadGestureMagnifyEvent, value); } } /// /// Occurs when the user uses rotate gesture on a trackpad and pointer is over the control. /// Works only on macOS. /// - public event EventHandler PointerRotateGesture + public event EventHandler PointerTouchPadGestureRotate { - add { AddHandler(PointerRotateGestureEvent, value); } - remove { RemoveHandler(PointerRotateGestureEvent, value); } + add { AddHandler(PointerTouchPadGestureRotateEvent, value); } + remove { RemoveHandler(PointerTouchPadGestureRotateEvent, value); } } /// /// Occurs when the user uses swipe gesture on a trackpad and pointer is over the control. /// Works only on macOS. /// - public event EventHandler PointerSwipeGesture + public event EventHandler PointerTouchPadGestureSwipe { - add { AddHandler(PointerSwipeGestureEvent, value); } - remove { RemoveHandler(PointerSwipeGestureEvent, value); } + add { AddHandler(PointerTouchPadGestureSwipeEvent, value); } + remove { RemoveHandler(PointerTouchPadGestureSwipeEvent, value); } } /// @@ -674,30 +665,6 @@ namespace Avalonia.Input protected virtual void OnPointerWheelChanged(PointerWheelEventArgs e) { } - - /// - /// Called before the trackpad event occurs. - /// - /// The event args. - protected virtual void OnPointerMagnifyGesture(PointerMagnifyGestureEventArgs e) - { - } - - /// - /// Called before the trackpad event occurs. - /// - /// The event args. - protected virtual void OnPointerRotateGesture(PointerRotateGestureEventArgs e) - { - } - - /// - /// Called before the trackpad event occurs. - /// - /// The event args. - protected virtual void OnPointerSwipeGesture(PointerSwipeGestureEventArgs e) - { - } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index c919242c78..7197397e99 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -355,7 +355,7 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerMagnifyGestureEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta.X); + var e = new PointerTouchPadGestureMagnifyEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta.X); source?.RaiseEvent(e); return e.Handled; @@ -376,7 +376,7 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerRotateGestureEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta.X); + var e = new PointerTouchPadGestureRotateEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta.X); source?.RaiseEvent(e); return e.Handled; @@ -397,7 +397,7 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerSwipeGestureEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); + var e = new PointerTouchPadGestureSwipeEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); source?.RaiseEvent(e); return e.Handled; diff --git a/src/Avalonia.Input/PointerRotateGestureEventArgs.cs b/src/Avalonia.Input/PointerTouchPadGestureMagnifyEventArgs.cs similarity index 55% rename from src/Avalonia.Input/PointerRotateGestureEventArgs.cs rename to src/Avalonia.Input/PointerTouchPadGestureMagnifyEventArgs.cs index 4bf1ffaaaa..d55d23d2c8 100644 --- a/src/Avalonia.Input/PointerRotateGestureEventArgs.cs +++ b/src/Avalonia.Input/PointerTouchPadGestureMagnifyEventArgs.cs @@ -3,14 +3,14 @@ using Avalonia.VisualTree; namespace Avalonia.Input { - public class PointerRotateGestureEventArgs : PointerEventArgs + public class PointerTouchPadGestureMagnifyEventArgs : PointerEventArgs { public double Delta { get; set; } - public PointerRotateGestureEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, + public PointerTouchPadGestureMagnifyEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, double delta) - : base(InputElement.PointerRotateGestureEvent, source, pointer, rootVisual, rootVisualPosition, + : base(InputElement.PointerTouchPadGestureMagnifyEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) { Delta = delta; diff --git a/src/Avalonia.Input/PointerMagnifyGestureEventArgs.cs b/src/Avalonia.Input/PointerTouchPadGestureRotateEventArgs.cs similarity index 55% rename from src/Avalonia.Input/PointerMagnifyGestureEventArgs.cs rename to src/Avalonia.Input/PointerTouchPadGestureRotateEventArgs.cs index 828307c5a7..8563cf6609 100644 --- a/src/Avalonia.Input/PointerMagnifyGestureEventArgs.cs +++ b/src/Avalonia.Input/PointerTouchPadGestureRotateEventArgs.cs @@ -3,14 +3,14 @@ using Avalonia.VisualTree; namespace Avalonia.Input { - public class PointerMagnifyGestureEventArgs : PointerEventArgs + public class PointerTouchPadGestureRotateEventArgs : PointerEventArgs { public double Delta { get; set; } - public PointerMagnifyGestureEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, + public PointerTouchPadGestureRotateEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, double delta) - : base(InputElement.PointerMagnifyGestureEvent, source, pointer, rootVisual, rootVisualPosition, + : base(InputElement.PointerTouchPadGestureRotateEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) { Delta = delta; diff --git a/src/Avalonia.Input/PointerSwipeGestureEventArgs.cs b/src/Avalonia.Input/PointerTouchPadGestureSwipeEventArgs.cs similarity index 55% rename from src/Avalonia.Input/PointerSwipeGestureEventArgs.cs rename to src/Avalonia.Input/PointerTouchPadGestureSwipeEventArgs.cs index c4994c8504..1d9c0d6189 100644 --- a/src/Avalonia.Input/PointerSwipeGestureEventArgs.cs +++ b/src/Avalonia.Input/PointerTouchPadGestureSwipeEventArgs.cs @@ -3,14 +3,14 @@ using Avalonia.VisualTree; namespace Avalonia.Input { - public class PointerSwipeGestureEventArgs : PointerEventArgs + public class PointerTouchPadGestureSwipeEventArgs : PointerEventArgs { public Vector Delta { get; set; } - public PointerSwipeGestureEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, + public PointerTouchPadGestureSwipeEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, Vector delta) - : base(InputElement.PointerSwipeGestureEvent, source, pointer, rootVisual, rootVisualPosition, + : base(InputElement.PointerTouchPadGestureSwipeEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) { Delta = delta; From 03d6ca9374f932a7d411dc3a9b2003fc0cb277eb Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 22 Dec 2021 00:33:26 -0500 Subject: [PATCH 005/134] Better fix for #6830 Previous numericupdown bugfix (see https://github.com/AvaloniaUI/Avalonia/pull/6830/files) introduced another bug (see https://github.com/AvaloniaUI/Avalonia/discussions/7230#discussioncomment-1855837). This PR solves both problems. --- src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml b/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml index 1e58259358..36ab07e3e3 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml @@ -38,7 +38,7 @@ BorderBrush="{TemplateBinding BorderBrush}" CornerRadius="{TemplateBinding CornerRadius}" Padding="0" - MinWidth="{TemplateBinding MinWidth}" + MinWidth="0" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" AllowSpin="{TemplateBinding AllowSpin}" @@ -50,7 +50,7 @@ BorderBrush="Transparent" Margin="-1" Padding="{TemplateBinding Padding}" - MinWidth="{TemplateBinding MinWidth}" + MinWidth="0" Foreground="{TemplateBinding Foreground}" FontSize="{TemplateBinding FontSize}" Watermark="{TemplateBinding Watermark}" From ffac3eb0278ea1281aea3aa281d910663c6f010d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Dec 2021 14:24:46 +0100 Subject: [PATCH 006/134] Added nullable annotations to Avalonia.Animation. --- src/Avalonia.Animation/Animatable.cs | 16 +++---- src/Avalonia.Animation/Animation.cs | 46 +++++++++++++------ src/Avalonia.Animation/AnimationInstance`1.cs | 33 +++++++------ src/Avalonia.Animation/AnimatorKeyFrame.cs | 20 ++++---- .../Animators/Animator`1.cs | 9 ++-- .../Avalonia.Animation.csproj | 4 ++ src/Avalonia.Animation/Clock.cs | 2 +- src/Avalonia.Animation/Cue.cs | 8 ++-- .../DisposeAnimationInstanceSubject.cs | 8 ++-- src/Avalonia.Animation/Easing/Easing.cs | 4 +- .../Easing/EasingTypeConverter.cs | 4 +- src/Avalonia.Animation/IAnimation.cs | 2 +- src/Avalonia.Animation/IAnimationSetter.cs | 4 +- src/Avalonia.Animation/IAnimator.cs | 4 +- src/Avalonia.Animation/ITransition.cs | 2 +- src/Avalonia.Animation/IterationCount.cs | 2 +- .../IterationCountTypeConverter.cs | 4 +- src/Avalonia.Animation/KeyFrame.cs | 4 +- .../KeySplineTypeConverter.cs | 4 +- src/Avalonia.Animation/Transition.cs | 21 +++++++-- src/Avalonia.Animation/TransitionInstance.cs | 6 +-- .../Animation/Animators/BaseBrushAnimator.cs | 7 ++- .../Animators/GradientBrushAnimator.cs | 5 ++ .../Animators/SolidColorBrushAnimator.cs | 5 ++ .../Animation/Animators/TransformAnimator.cs | 7 ++- 25 files changed, 142 insertions(+), 89 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 4811028f85..50fc5ac73b 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -157,7 +157,7 @@ namespace Avalonia.Animation state.Instance?.Dispose(); state.Instance = transition.Apply( this, - Clock ?? AvaloniaLocator.Current.GetService(), + Clock ?? AvaloniaLocator.Current.GetRequiredService(), oldValue, newValue); return; @@ -169,7 +169,7 @@ namespace Avalonia.Animation base.OnPropertyChangedCore(change); } - private void TransitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + private void TransitionsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (!_transitionsEnabled) { @@ -179,14 +179,14 @@ namespace Avalonia.Animation switch (e.Action) { case NotifyCollectionChangedAction.Add: - AddTransitions(e.NewItems); + AddTransitions(e.NewItems!); break; case NotifyCollectionChangedAction.Remove: - RemoveTransitions(e.OldItems); + RemoveTransitions(e.OldItems!); break; case NotifyCollectionChangedAction.Replace: - RemoveTransitions(e.OldItems); - AddTransitions(e.NewItems); + RemoveTransitions(e.OldItems!); + AddTransitions(e.NewItems!); break; case NotifyCollectionChangedAction.Reset: throw new NotSupportedException("Transitions collection cannot be reset."); @@ -204,7 +204,7 @@ namespace Avalonia.Animation for (var i = 0; i < items.Count; ++i) { - var t = (ITransition)items[i]; + var t = (ITransition)items[i]!; _transitionState.Add(t, new TransitionState { @@ -222,7 +222,7 @@ namespace Avalonia.Animation for (var i = 0; i < items.Count; ++i) { - var t = (ITransition)items[i]; + var t = (ITransition)items[i]!; if (_transitionState.TryGetValue(t, out var state)) { diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index a4515db514..03b2d17e44 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -203,7 +203,7 @@ namespace Avalonia.Animation /// /// The animation setter. /// The property animator type. - public static Type GetAnimator(IAnimationSetter setter) + public static Type? GetAnimator(IAnimationSetter setter) { if (s_animators.TryGetValue(setter, out var type)) { @@ -254,7 +254,7 @@ namespace Avalonia.Animation Animators.Insert(0, (condition, typeof(TAnimator))); } - private static Type GetAnimatorType(AvaloniaProperty property) + private static Type? GetAnimatorType(AvaloniaProperty property) { foreach (var (condition, type) in Animators) { @@ -276,6 +276,11 @@ namespace Avalonia.Animation { foreach (var setter in keyframe.Setters) { + if (setter.Property is null) + { + throw new InvalidOperationException("No Setter property assigned."); + } + var handler = Animation.GetAnimator(setter) ?? GetAnimatorType(setter.Property); if (handler == null) @@ -305,7 +310,7 @@ namespace Avalonia.Animation foreach (var (handlerType, property) in handlerList) { - var newInstance = (IAnimator)Activator.CreateInstance(handlerType); + var newInstance = (IAnimator)Activator.CreateInstance(handlerType)!; newInstance.Property = property; newAnimatorInstances.Add(newInstance); } @@ -321,32 +326,43 @@ namespace Avalonia.Animation } /// - public IDisposable Apply(Animatable control, IClock clock, IObservable match, Action onComplete) + public IDisposable Apply(Animatable control, IClock? clock, IObservable match, Action? onComplete) { var (animators, subscriptions) = InterpretKeyframes(control); if (animators.Count == 1) { - subscriptions.Add(animators[0].Apply(this, control, clock, match, onComplete)); + var subscription = animators[0].Apply(this, control, clock, match, onComplete); + + if (subscription is not null) + { + subscriptions.Add(subscription); + } } else { var completionTasks = onComplete != null ? new List() : null; foreach (IAnimator animator in animators) { - Action animatorOnComplete = null; + Action? animatorOnComplete = null; if (onComplete != null) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); animatorOnComplete = () => tcs.SetResult(null); - completionTasks.Add(tcs.Task); + completionTasks!.Add(tcs.Task); + } + + var subscription = animator.Apply(this, control, clock, match, animatorOnComplete); + + if (subscription is not null) + { + subscriptions.Add(subscription); } - subscriptions.Add(animator.Apply(this, control, clock, match, animatorOnComplete)); } if (onComplete != null) { - Task.WhenAll(completionTasks).ContinueWith( - (_, state) => ((Action)state).Invoke(), + Task.WhenAll(completionTasks!).ContinueWith( + (_, state) => ((Action)state!).Invoke(), onComplete); } } @@ -354,25 +370,25 @@ namespace Avalonia.Animation } /// - public Task RunAsync(Animatable control, IClock clock = null) + public Task RunAsync(Animatable control, IClock? clock = null) { return RunAsync(control, clock, default); } /// - public Task RunAsync(Animatable control, IClock clock = null, CancellationToken cancellationToken = default) + public Task RunAsync(Animatable control, IClock? clock = null, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) { return Task.CompletedTask; } - var run = new TaskCompletionSource(); + var run = new TaskCompletionSource(); if (this.IterationCount == IterationCount.Infinite) run.SetException(new InvalidOperationException("Looping animations must not use the Run method.")); - IDisposable subscriptions = null, cancellation = null; + IDisposable? subscriptions = null, cancellation = null; subscriptions = this.Apply(control, clock, Observable.Return(true), () => { run.TrySetResult(null); diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs index cf79640150..52cd4b324f 100644 --- a/src/Avalonia.Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Animation/AnimationInstance`1.cs @@ -31,15 +31,15 @@ namespace Avalonia.Animation private TimeSpan _initialDelay; private TimeSpan _iterationDelay; private TimeSpan _duration; - private Easings.Easing _easeFunc; - private Action _onCompleteAction; + private Easings.Easing? _easeFunc; + private Action? _onCompleteAction; private Func _interpolator; - private IDisposable _timerSub; + private IDisposable? _timerSub; private readonly IClock _baseClock; - private IClock _clock; - private EventHandler _propertyChangedDelegate; + private IClock? _clock; + private EventHandler? _propertyChangedDelegate; - public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action OnComplete, Func Interpolator) + public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action? OnComplete, Func Interpolator) { _animator = animator; _animation = animation; @@ -47,6 +47,9 @@ namespace Avalonia.Animation _onCompleteAction = OnComplete; _interpolator = Interpolator; _baseClock = baseClock; + _lastInterpValue = default!; + _firstKFValue = default!; + _neutralValue = default!; FetchProperties(); } @@ -82,7 +85,7 @@ namespace Avalonia.Animation _targetControl.PropertyChanged -= _propertyChangedDelegate; _timerSub?.Dispose(); - _clock.PlayState = PlayState.Stop; + _clock!.PlayState = PlayState.Stop; } protected override void Subscribed() @@ -108,6 +111,8 @@ namespace Avalonia.Animation private void ApplyFinalFill() { + if (_animator.Property is null) + throw new InvalidOperationException("Animator has no property specified."); if (_fillMode == FillMode.Forward || _fillMode == FillMode.Both) _targetControl.SetValue(_animator.Property, _lastInterpValue, BindingPriority.LocalValue); } @@ -130,12 +135,12 @@ namespace Avalonia.Animation private void DoPlayStates() { - if (_clock.PlayState == PlayState.Stop || _baseClock.PlayState == PlayState.Stop) + if (_clock!.PlayState == PlayState.Stop || _baseClock.PlayState == PlayState.Stop) DoComplete(); if (!_gotFirstKFValue) { - _firstKFValue = (T)_animator.First().Value; + _firstKFValue = (T)_animator.First().Value!; _gotFirstKFValue = true; } } @@ -169,7 +174,7 @@ namespace Avalonia.Animation // and snap the last iteration value to exact values. if ((_currentIteration + 1) > _iterationCount) { - var easedTime = _easeFunc.Ease(_playbackReversed ? 0.0 : 1.0); + var easedTime = _easeFunc!.Ease(_playbackReversed ? 0.0 : 1.0); _lastInterpValue = _interpolator(easedTime, _neutralValue); DoComplete(); } @@ -203,7 +208,7 @@ namespace Avalonia.Animation normalizedTime = 1 - normalizedTime; // Ease and interpolate - var easedTime = _easeFunc.Ease(normalizedTime); + var easedTime = _easeFunc!.Ease(normalizedTime); _lastInterpValue = _interpolator(easedTime, _neutralValue); PublishNext(_lastInterpValue); @@ -223,14 +228,14 @@ namespace Avalonia.Animation private void UpdateNeutralValue() { - var property = _animator.Property; + var property = _animator.Property ?? throw new InvalidOperationException("Animator has no property specified."); var baseValue = _targetControl.GetBaseValue(property, BindingPriority.LocalValue); _neutralValue = baseValue != AvaloniaProperty.UnsetValue ? - (T)baseValue : (T)_targetControl.GetValue(property); + (T)baseValue! : (T)_targetControl.GetValue(property)!; } - private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + private void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property == _animator.Property && e.Priority > BindingPriority.Animation) { diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs index f6a0c12be4..8af31f2948 100644 --- a/src/Avalonia.Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Animation/AnimatorKeyFrame.cs @@ -12,22 +12,22 @@ namespace Avalonia.Animation /// public class AnimatorKeyFrame : AvaloniaObject { - public static readonly DirectProperty ValueProperty = - AvaloniaProperty.RegisterDirect(nameof(Value), k => k.Value, (k, v) => k.Value = v); + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect(nameof(Value), k => k.Value, (k, v) => k.Value = v); public AnimatorKeyFrame() { } - public AnimatorKeyFrame(Type animatorType, Cue cue) + public AnimatorKeyFrame(Type? animatorType, Cue cue) { AnimatorType = animatorType; Cue = cue; KeySpline = null; } - public AnimatorKeyFrame(Type animatorType, Cue cue, KeySpline keySpline) + public AnimatorKeyFrame(Type? animatorType, Cue cue, KeySpline? keySpline) { AnimatorType = animatorType; Cue = cue; @@ -35,14 +35,14 @@ namespace Avalonia.Animation } internal bool isNeutral; - public Type AnimatorType { get; } + public Type? AnimatorType { get; } public Cue Cue { get; } - public KeySpline KeySpline { get; } - public AvaloniaProperty Property { get; private set; } + public KeySpline? KeySpline { get; } + public AvaloniaProperty? Property { get; private set; } - private object _value; + private object? _value; - public object Value + public object? Value { get => _value; set => SetAndRaise(ValueProperty, ref _value, value); @@ -80,7 +80,7 @@ namespace Avalonia.Animation throw new InvalidCastException($"KeyFrame value doesnt match property type."); } - return (T)typeConv.ConvertTo(Value, typeof(T)); + return (T)typeConv.ConvertTo(Value, typeof(T))!; } } } diff --git a/src/Avalonia.Animation/Animators/Animator`1.cs b/src/Avalonia.Animation/Animators/Animator`1.cs index 23afa76bf6..248ca61c1d 100644 --- a/src/Avalonia.Animation/Animators/Animator`1.cs +++ b/src/Avalonia.Animation/Animators/Animator`1.cs @@ -24,7 +24,7 @@ namespace Avalonia.Animation.Animators /// /// Gets or sets the target property for the keyframe. /// - public AvaloniaProperty Property { get; set; } + public AvaloniaProperty? Property { get; set; } public Animator() { @@ -33,7 +33,7 @@ namespace Avalonia.Animation.Animators } /// - public virtual IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable match, Action onComplete) + public virtual IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable match, Action? onComplete) { if (!_isVerifiedAndConverted) VerifyConvertKeyFrames(); @@ -106,13 +106,16 @@ namespace Avalonia.Animation.Animators public virtual IDisposable BindAnimation(Animatable control, IObservable instance) { + if (Property is null) + throw new InvalidOperationException("Animator has no property specified."); + return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); } /// /// Runs the KeyFrames Animation. /// - internal IDisposable Run(Animation animation, Animatable control, IClock clock, Action onComplete) + internal IDisposable Run(Animation animation, Animatable control, IClock? clock, Action? onComplete) { var instance = new AnimationInstance( animation, diff --git a/src/Avalonia.Animation/Avalonia.Animation.csproj b/src/Avalonia.Animation/Avalonia.Animation.csproj index 85938ad958..9e3758658c 100644 --- a/src/Avalonia.Animation/Avalonia.Animation.csproj +++ b/src/Avalonia.Animation/Avalonia.Animation.csproj @@ -2,9 +2,13 @@ netstandard2.0;net6.0 + + + + diff --git a/src/Avalonia.Animation/Clock.cs b/src/Avalonia.Animation/Clock.cs index 5c2b7ce0dd..5afd2ae705 100644 --- a/src/Avalonia.Animation/Clock.cs +++ b/src/Avalonia.Animation/Clock.cs @@ -4,7 +4,7 @@ namespace Avalonia.Animation { public class Clock : ClockBase { - public static IClock GlobalClock => AvaloniaLocator.Current.GetService(); + public static IClock GlobalClock => AvaloniaLocator.Current.GetRequiredService(); private readonly IDisposable _parentSubscription; diff --git a/src/Avalonia.Animation/Cue.cs b/src/Avalonia.Animation/Cue.cs index 7da7a9382b..6578148b07 100644 --- a/src/Avalonia.Animation/Cue.cs +++ b/src/Avalonia.Animation/Cue.cs @@ -30,7 +30,7 @@ namespace Avalonia.Animation /// /// Parses a string to a object. /// - public static Cue Parse(string value, CultureInfo culture) + public static Cue Parse(string value, CultureInfo? culture) { string v = value; @@ -72,14 +72,14 @@ namespace Avalonia.Animation public class CueTypeConverter : TypeConverter { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string); } - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { return Cue.Parse((string)value, culture); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs b/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs index 696f43d006..7283eaeedf 100644 --- a/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs +++ b/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs @@ -8,15 +8,15 @@ namespace Avalonia.Animation /// internal class DisposeAnimationInstanceSubject : IObserver, IDisposable { - private IDisposable _lastInstance; + private IDisposable? _lastInstance; private bool _lastMatch; private Animator _animator; private Animation _animation; private Animatable _control; - private Action _onComplete; - private IClock _clock; + private Action? _onComplete; + private IClock? _clock; - public DisposeAnimationInstanceSubject(Animator animator, Animation animation, Animatable control, IClock clock, Action onComplete) + public DisposeAnimationInstanceSubject(Animator animator, Animation animation, Animatable control, IClock? clock, Action? onComplete) { this._animator = animator; this._animation = animation; diff --git a/src/Avalonia.Animation/Easing/Easing.cs b/src/Avalonia.Animation/Easing/Easing.cs index e006459652..2f4b93dab1 100644 --- a/src/Avalonia.Animation/Easing/Easing.cs +++ b/src/Avalonia.Animation/Easing/Easing.cs @@ -15,7 +15,7 @@ namespace Avalonia.Animation.Easings /// public abstract double Ease(double progress); - static Dictionary _easingTypes; + static Dictionary? _easingTypes; static readonly Type s_thisType = typeof(Easing); @@ -48,7 +48,7 @@ namespace Avalonia.Animation.Easings if (_easingTypes.ContainsKey(e)) { var type = _easingTypes[e]; - return (Easing)Activator.CreateInstance(type); + return (Easing)Activator.CreateInstance(type)!; } else { diff --git a/src/Avalonia.Animation/Easing/EasingTypeConverter.cs b/src/Avalonia.Animation/Easing/EasingTypeConverter.cs index 6613f6d393..3d67d54a6f 100644 --- a/src/Avalonia.Animation/Easing/EasingTypeConverter.cs +++ b/src/Avalonia.Animation/Easing/EasingTypeConverter.cs @@ -6,12 +6,12 @@ namespace Avalonia.Animation.Easings { public class EasingTypeConverter : TypeConverter { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string); } - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { return Easing.Parse((string)value); } diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs index d037834630..436a765d27 100644 --- a/src/Avalonia.Animation/IAnimation.cs +++ b/src/Avalonia.Animation/IAnimation.cs @@ -12,7 +12,7 @@ namespace Avalonia.Animation /// /// Apply the animation to the specified control and run it when produces true. /// - IDisposable Apply(Animatable control, IClock clock, IObservable match, Action onComplete = null); + IDisposable Apply(Animatable control, IClock? clock, IObservable match, Action? onComplete = null); /// /// Run the animation on the specified control. diff --git a/src/Avalonia.Animation/IAnimationSetter.cs b/src/Avalonia.Animation/IAnimationSetter.cs index 2d22377286..6a1d3539e2 100644 --- a/src/Avalonia.Animation/IAnimationSetter.cs +++ b/src/Avalonia.Animation/IAnimationSetter.cs @@ -2,7 +2,7 @@ namespace Avalonia.Animation { public interface IAnimationSetter { - AvaloniaProperty Property { get; set; } - object Value { get; set; } + AvaloniaProperty? Property { get; set; } + object? Value { get; set; } } } diff --git a/src/Avalonia.Animation/IAnimator.cs b/src/Avalonia.Animation/IAnimator.cs index d0fb173c54..f64ac9f913 100644 --- a/src/Avalonia.Animation/IAnimator.cs +++ b/src/Avalonia.Animation/IAnimator.cs @@ -11,11 +11,11 @@ namespace Avalonia.Animation /// /// The target property. /// - AvaloniaProperty Property {get; set;} + AvaloniaProperty? Property {get; set;} /// /// Applies the current KeyFrame group to the specified control. /// - IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable match, Action onComplete); + IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable match, Action? onComplete); } } diff --git a/src/Avalonia.Animation/ITransition.cs b/src/Avalonia.Animation/ITransition.cs index ade2ec8b9e..241ca208d1 100644 --- a/src/Avalonia.Animation/ITransition.cs +++ b/src/Avalonia.Animation/ITransition.cs @@ -10,7 +10,7 @@ namespace Avalonia.Animation /// /// Applies the transition to the specified . /// - IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue); + IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue); /// /// Gets the property to be animated. diff --git a/src/Avalonia.Animation/IterationCount.cs b/src/Avalonia.Animation/IterationCount.cs index 9463718608..3b52cdab49 100644 --- a/src/Avalonia.Animation/IterationCount.cs +++ b/src/Avalonia.Animation/IterationCount.cs @@ -97,7 +97,7 @@ namespace Avalonia.Animation /// /// The object with which to test equality. /// True if the objects are equal, otherwise false. - public override bool Equals(object o) + public override bool Equals(object? o) { if (o == null) { diff --git a/src/Avalonia.Animation/IterationCountTypeConverter.cs b/src/Avalonia.Animation/IterationCountTypeConverter.cs index 1c63f8cdf1..f64972ff5c 100644 --- a/src/Avalonia.Animation/IterationCountTypeConverter.cs +++ b/src/Avalonia.Animation/IterationCountTypeConverter.cs @@ -6,12 +6,12 @@ namespace Avalonia.Animation { public class IterationCountTypeConverter : TypeConverter { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string); } - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { return IterationCount.Parse((string)value); } diff --git a/src/Avalonia.Animation/KeyFrame.cs b/src/Avalonia.Animation/KeyFrame.cs index c2cc1aa051..3ab7a70d90 100644 --- a/src/Avalonia.Animation/KeyFrame.cs +++ b/src/Avalonia.Animation/KeyFrame.cs @@ -19,7 +19,7 @@ namespace Avalonia.Animation { private TimeSpan _ktimeSpan; private Cue _kCue; - private KeySpline _kKeySpline; + private KeySpline? _kKeySpline; public KeyFrame() { @@ -79,7 +79,7 @@ namespace Avalonia.Animation /// Gets or sets the KeySpline of this . /// /// The key spline. - public KeySpline KeySpline + public KeySpline? KeySpline { get { diff --git a/src/Avalonia.Animation/KeySplineTypeConverter.cs b/src/Avalonia.Animation/KeySplineTypeConverter.cs index b026206e5f..eecad3c3ac 100644 --- a/src/Avalonia.Animation/KeySplineTypeConverter.cs +++ b/src/Avalonia.Animation/KeySplineTypeConverter.cs @@ -12,12 +12,12 @@ namespace Avalonia.Animation /// public class KeySplineTypeConverter : TypeConverter { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string); } - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { return KeySpline.Parse((string)value, CultureInfo.InvariantCulture); } diff --git a/src/Avalonia.Animation/Transition.cs b/src/Avalonia.Animation/Transition.cs index 4115c95c0f..d307f348c4 100644 --- a/src/Avalonia.Animation/Transition.cs +++ b/src/Avalonia.Animation/Transition.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Animation.Easings; namespace Avalonia.Animation @@ -8,7 +9,7 @@ namespace Avalonia.Animation /// public abstract class Transition : AvaloniaObject, ITransition { - private AvaloniaProperty _prop; + private AvaloniaProperty? _prop; /// /// Gets or sets the duration of the transition. @@ -26,7 +27,8 @@ namespace Avalonia.Animation public Easing Easing { get; set; } = new LinearEasing(); /// - public AvaloniaProperty Property + [DisallowNull] + public AvaloniaProperty? Property { get { @@ -42,16 +44,25 @@ namespace Avalonia.Animation } } + AvaloniaProperty ITransition.Property + { + get => Property ?? throw new InvalidOperationException("Transition has no property specified."); + set => Property = value; + } + /// /// Apply interpolation to the property. /// public abstract IObservable DoTransition(IObservable progress, T oldValue, T newValue); /// - public virtual IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue) + public virtual IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue) { - var transition = DoTransition(new TransitionInstance(clock, Delay, Duration), (T)oldValue, (T)newValue); + if (Property is null) + throw new InvalidOperationException("Transition has no property specified."); + + var transition = DoTransition(new TransitionInstance(clock, Delay, Duration), (T)oldValue!, (T)newValue!); return control.Bind((AvaloniaProperty)Property, transition, Data.BindingPriority.Animation); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index b522d1961e..9c9494ff87 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/src/Avalonia.Animation/TransitionInstance.cs @@ -10,11 +10,11 @@ namespace Avalonia.Animation /// internal class TransitionInstance : SingleSubscriberObservableBase, IObserver { - private IDisposable _timerSubscription; + private IDisposable? _timerSubscription; private TimeSpan _delay; private TimeSpan _duration; private readonly IClock _baseClock; - private TransitionClock _clock; + private TransitionClock? _clock; public TransitionInstance(IClock clock, TimeSpan delay, TimeSpan duration) { @@ -67,7 +67,7 @@ namespace Avalonia.Animation protected override void Unsubscribed() { _timerSubscription?.Dispose(); - _clock.PlayState = PlayState.Stop; + _clock!.PlayState = PlayState.Stop; } protected override void Subscribed() diff --git a/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs index a2c4b0313b..5f22254fb5 100644 --- a/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs @@ -38,8 +38,8 @@ namespace Avalonia.Animation.Animators } /// - public override IDisposable Apply(Animation animation, Animatable control, IClock clock, - IObservable match, Action onComplete) + public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock, + IObservable match, Action? onComplete) { if (TryCreateCustomRegisteredAnimator(out var animator) || TryCreateGradientAnimator(out animator) @@ -135,9 +135,8 @@ namespace Avalonia.Animation.Animators private bool TryCreateCustomRegisteredAnimator([NotNullWhen(true)] out IAnimator? animator) { - if (_brushAnimators.Count > 0) + if (_brushAnimators.Count > 0 && this[0].Value?.GetType() is Type firstKeyType) { - var firstKeyType = this[0].Value.GetType(); foreach (var (match, animatorType) in _brushAnimators) { if (!match(firstKeyType)) diff --git a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs index 864e12413f..0979de16d0 100644 --- a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs @@ -58,6 +58,11 @@ namespace Avalonia.Animation.Animators public override IDisposable BindAnimation(Animatable control, IObservable instance) { + if (Property is null) + { + throw new InvalidOperationException("Animator has no property specified."); + } + return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); } diff --git a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs index 7c6372aae2..57f9f3c1a5 100644 --- a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs @@ -24,6 +24,11 @@ namespace Avalonia.Animation.Animators public override IDisposable BindAnimation(Animatable control, IObservable instance) { + if (Property is null) + { + throw new InvalidOperationException("Animator has no property specified."); + } + return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); } } diff --git a/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs index 1d7bfd3748..34ec8ac503 100644 --- a/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs @@ -14,10 +14,15 @@ namespace Avalonia.Animation.Animators DoubleAnimator? _doubleAnimator; /// - public override IDisposable? Apply(Animation animation, Animatable control, IClock clock, IObservable obsMatch, Action onComplete) + public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable obsMatch, Action? onComplete) { var ctrl = (Visual)control; + if (Property is null) + { + throw new InvalidOperationException("Animator has no property specified."); + } + // Check if the Target Property is Transform derived. if (typeof(Transform).IsAssignableFrom(Property.OwnerType)) { From 98d061ec30b65b63ec98d9c45e52332d65a6db21 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Dec 2021 15:53:12 +0100 Subject: [PATCH 007/134] Fix failing unit tests. --- .../AnimatableTests.cs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs index 26d8059eec..1d5296bebd 100644 --- a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs +++ b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs @@ -36,7 +36,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Transition_Is_Not_Applied_To_Initial_Style() { - using (UnitTestApplication.Start(TestServices.RealStyler)) + using (Start()) { var target = CreateTarget(); var control = new Control @@ -74,6 +74,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Transition_Is_Applied_When_Local_Value_Changes() { + using var app = Start(); var target = CreateTarget(); var control = CreateControl(target.Object); @@ -170,6 +171,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Transition_Is_Not_Applied_When_StyleTrigger_Changes_With_LocalValue_Present() { + using var app = Start(); var target = CreateTarget(); var control = CreateControl(target.Object); @@ -195,6 +197,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Transition_Is_Disposed_When_Local_Value_Changes() { + using var app = Start(); var target = CreateTarget(); var control = CreateControl(target.Object); var sub = new Mock(); @@ -211,6 +214,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void New_Transition_Is_Applied_When_Local_Value_Changes() { + using var app = Start(); var target = CreateTarget(); var control = CreateControl(target.Object); @@ -239,6 +243,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Transition_Is_Not_Applied_When_Removed_From_Visual_Tree() { + using var app = Start(); var target = CreateTarget(); var control = CreateControl(target.Object); @@ -266,6 +271,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Animation_Is_Cancelled_When_Transition_Removed() { + using var app = Start(); var target = CreateTarget(); var control = CreateControl(target.Object); var sub = new Mock(); @@ -285,7 +291,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Animation_Is_Cancelled_When_New_Style_Activates() { - using (UnitTestApplication.Start(TestServices.RealStyler)) + using (Start()) { var target = CreateTarget(); var control = CreateStyledControl(target.Object); @@ -301,7 +307,7 @@ namespace Avalonia.Animation.UnitTests target.Verify(x => x.Apply( control, - It.IsAny(), + It.IsAny(), 1.0, 0.5), Times.Once); @@ -315,7 +321,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Transition_From_Style_Trigger_Is_Applied() { - using (UnitTestApplication.Start(TestServices.RealStyler)) + using (Start()) { var target = CreateTransition(Control.WidthProperty); var control = CreateStyledControl(transition2: target.Object); @@ -326,7 +332,7 @@ namespace Avalonia.Animation.UnitTests target.Verify(x => x.Apply( control, - It.IsAny(), + It.IsAny(), double.NaN, 100.0), Times.Once); @@ -337,7 +343,7 @@ namespace Avalonia.Animation.UnitTests public void Replacing_Transitions_During_Animation_Does_Not_Throw_KeyNotFound() { // Issue #4059 - using (UnitTestApplication.Start(TestServices.RealStyler)) + using (Start()) { Border target; var clock = new TestClock(); @@ -428,6 +434,13 @@ namespace Avalonia.Animation.UnitTests control.EndBatchUpdate(); } + private static IDisposable Start() + { + var clock = new MockGlobalClock(); + var services = TestServices.RealStyler.With(globalClock: clock); + return UnitTestApplication.Start(services); + } + private static Mock CreateTarget() { return CreateTransition(Visual.OpacityProperty); From 9275e599090bb3a3428411ef8c8c4107f2624fa6 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 22 Dec 2021 21:36:48 -0500 Subject: [PATCH 008/134] Do not recalculate pointer popup position on parent moved --- src/Avalonia.Controls/Primitives/Popup.cs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index ffab7f86d1..a47149a9e0 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -429,16 +429,20 @@ namespace Avalonia.Controls.Primitives (x, handler) => x.LostFocus += handler, (x, handler) => x.LostFocus -= handler).DisposeWith(handlerCleanup); - SubscribeToEventHandler>(window.PlatformImpl, WindowPositionChanged, - (x, handler) => x.PositionChanged += handler, - (x, handler) => x.PositionChanged -= handler).DisposeWith(handlerCleanup); - - if (placementTarget is Layoutable layoutTarget) + // Recalculate popup position on parent moved/resized, but not if placement was on pointer + if (PlacementMode != PlacementMode.Pointer) { - // If the placement target is moved, update the popup position - SubscribeToEventHandler(layoutTarget, PlacementTargetLayoutUpdated, - (x, handler) => x.LayoutUpdated += handler, - (x, handler) => x.LayoutUpdated -= handler).DisposeWith(handlerCleanup); + SubscribeToEventHandler>(window.PlatformImpl, WindowPositionChanged, + (x, handler) => x.PositionChanged += handler, + (x, handler) => x.PositionChanged -= handler).DisposeWith(handlerCleanup); + + if (placementTarget is Layoutable layoutTarget) + { + // If the placement target is moved, update the popup position + SubscribeToEventHandler(layoutTarget, PlacementTargetLayoutUpdated, + (x, handler) => x.LayoutUpdated += handler, + (x, handler) => x.LayoutUpdated -= handler).DisposeWith(handlerCleanup); + } } } else if (topLevel is PopupRoot parentPopupRoot) From 50a97dc22f4f48090dd2a8f01e20aa71c8a586df Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 22 Dec 2021 21:37:05 -0500 Subject: [PATCH 009/134] Add more popup tests --- .../Primitives/PopupTests.cs | 237 +++++++++++++++++- 1 file changed, 224 insertions(+), 13 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 24e4631aff..9c1822ff5c 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -16,6 +16,8 @@ using Avalonia.VisualTree; using Xunit; using Avalonia.Input; using Avalonia.Rendering; +using System.Threading.Tasks; +using Avalonia.Threading; namespace Avalonia.Controls.UnitTests.Primitives { @@ -597,27 +599,44 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Popup_Should_Follow_Placement_Target_On_Window_Move() + public void Popup_Should_Not_Follow_Placement_Target_On_Window_Move_If_Pointer() { using (CreateServices()) { - var popup = new Popup { Width = 400, Height = 200 }; + var popup = new Popup + { + Width = 400, + Height = 200, + PlacementMode = PlacementMode.Pointer + }; var window = PreparedWindow(popup); + window.Show(); popup.Open(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + var raised = false; if (popup.Host is PopupRoot popupRoot) { - // Moving the window must move the popup (screen coordinates have changed) - var raised = false; popupRoot.PositionChanged += (_, args) => { - Assert.Equal(new PixelPoint(10, 10), args.Point); raised = true; }; - window.Position = new PixelPoint(10, 10); - Assert.True(raised); } + else if (popup.Host is OverlayPopupHost overlayPopupHost) + { + overlayPopupHost.PropertyChanged += (_, args) => + { + if (args.Property == Canvas.TopProperty + || args.Property == Canvas.LeftProperty) + { + raised = true; + } + }; + } + window.Position = new PixelPoint(10, 10); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + Assert.False(raised); } } @@ -634,30 +653,222 @@ namespace Avalonia.Controls.UnitTests.Primitives HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center }; - var popup = new Popup() { PlacementTarget = placementTarget, Width = 10, Height = 10 }; + var popup = new Popup() + { + PlacementTarget = placementTarget, + PlacementMode = PlacementMode.Bottom, + Width = 10, + Height = 10 + }; ((ISetLogicalParent)popup).SetParent(popup.PlacementTarget); var window = PreparedWindow(placementTarget); window.Show(); popup.Open(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); // The target's initial placement is (395,295) which is a 10x10 panel centered in a 800x600 window Assert.Equal(placementTarget.Bounds, new Rect(395D, 295D, 10, 10)); + var raised = false; + // Resizing the window to 700x500 must move the popup to (345,255) as this is the new + // location of the placement target if (popup.Host is PopupRoot popupRoot) { - // Resizing the window to 700x500 must move the popup to (345,255) as this is the new - // location of the placement target - var raised = false; popupRoot.PositionChanged += (_, args) => { Assert.Equal(new PixelPoint(345, 255), args.Point); raised = true; }; - window.PlatformImpl?.Resize(new Size(700D, 500D), PlatformResizeReason.Unspecified); - Assert.True(raised); } + else if (popup.Host is OverlayPopupHost overlayPopupHost) + { + overlayPopupHost.PropertyChanged += (_, args) => + { + if ((args.Property == Canvas.TopProperty + || args.Property == Canvas.LeftProperty) + && Canvas.GetLeft(overlayPopupHost) == 345 + && Canvas.GetTop(overlayPopupHost) == 255) + { + raised = true; + } + }; + } + window.PlatformImpl?.Resize(new Size(700D, 500D), PlatformResizeReason.Unspecified); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + Assert.True(raised); + } + } + + [Fact] + public void Popup_Should_Not_Follow_Placement_Target_On_Window_Resize_If_Pointer_If_Pointer() + { + using (CreateServices()) + { + + var placementTarget = new Panel() + { + Width = 10, + Height = 10, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + var popup = new Popup() + { + PlacementTarget = placementTarget, + PlacementMode = PlacementMode.Pointer, + Width = 10, + Height = 10 + }; + ((ISetLogicalParent)popup).SetParent(popup.PlacementTarget); + + var window = PreparedWindow(placementTarget); + window.Show(); + popup.Open(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + + // The target's initial placement is (395,295) which is a 10x10 panel centered in a 800x600 window + Assert.Equal(placementTarget.Bounds, new Rect(395D, 295D, 10, 10)); + + var raised = false; + if (popup.Host is PopupRoot popupRoot) + { + popupRoot.PositionChanged += (_, args) => + { + raised = true; + }; + + } + else if (popup.Host is OverlayPopupHost overlayPopupHost) + { + overlayPopupHost.PropertyChanged += (_, args) => + { + if (args.Property == Canvas.TopProperty + || args.Property == Canvas.LeftProperty) + { + raised = true; + } + }; + } + window.PlatformImpl?.Resize(new Size(700D, 500D), PlatformResizeReason.Unspecified); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + Assert.False(raised); + } + } + + [Fact] + public void Popup_Should_Follow_Placement_Target_On_Target_Moved() + { + using (CreateServices()) + { + var placementTarget = new Panel() + { + Width = 10, + Height = 10, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + var popup = new Popup() + { + PlacementTarget = placementTarget, + PlacementMode = PlacementMode.Bottom, + Width = 10, + Height = 10 + }; + ((ISetLogicalParent)popup).SetParent(popup.PlacementTarget); + + var window = PreparedWindow(placementTarget); + window.Show(); + popup.Open(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + + // The target's initial placement is (395,295) which is a 10x10 panel centered in a 800x600 window + Assert.Equal(placementTarget.Bounds, new Rect(395D, 295D, 10, 10)); + + var raised = false; + // Margin will move placement target + if (popup.Host is PopupRoot popupRoot) + { + popupRoot.PositionChanged += (_, args) => + { + Assert.Equal(new PixelPoint(400, 305), args.Point); + raised = true; + }; + + } + else if (popup.Host is OverlayPopupHost overlayPopupHost) + { + overlayPopupHost.PropertyChanged += (_, args) => + { + if ((args.Property == Canvas.TopProperty + || args.Property == Canvas.LeftProperty) + && Canvas.GetLeft(overlayPopupHost) == 400 + && Canvas.GetTop(overlayPopupHost) == 305) + { + raised = true; + } + }; + } + placementTarget.Margin = new Thickness(10, 0, 0, 0); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + Assert.True(raised); + } + } + + [Fact] + public void Popup_Should_Not_Follow_Placement_Target_On_Target_Moved_If_Pointer() + { + using (CreateServices()) + { + + var placementTarget = new Panel() + { + Width = 10, + Height = 10, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + var popup = new Popup() + { + PlacementTarget = placementTarget, + PlacementMode = PlacementMode.Pointer, + Width = 10, + Height = 10 + }; + ((ISetLogicalParent)popup).SetParent(popup.PlacementTarget); + + var window = PreparedWindow(placementTarget); + window.Show(); + popup.Open(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + + // The target's initial placement is (395,295) which is a 10x10 panel centered in a 800x600 window + Assert.Equal(placementTarget.Bounds, new Rect(395D, 295D, 10, 10)); + + var raised = false; + if (popup.Host is PopupRoot popupRoot) + { + popupRoot.PositionChanged += (_, args) => + { + raised = true; + }; + + } + else if (popup.Host is OverlayPopupHost overlayPopupHost) + { + overlayPopupHost.PropertyChanged += (_, args) => + { + if (args.Property == Canvas.TopProperty + || args.Property == Canvas.LeftProperty) + { + raised = true; + } + }; + } + placementTarget.Margin = new Thickness(10, 0, 0, 0); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + Assert.False(raised); } } From 878397978b9b8a25a1c26abc45b8000b2f0e7aff Mon Sep 17 00:00:00 2001 From: abdesol Date: Thu, 30 Dec 2021 20:49:13 +0300 Subject: [PATCH 010/134] Added Progress text to ProgressBar.xaml from Avalonia.Themes.Fluent --- src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml index 7ad75588e1..a8bca77d3e 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml @@ -21,6 +21,7 @@ + @@ -29,7 +30,11 @@ - + + + + + From 7ef5296898357b18164d3137341a9ad98451ed79 Mon Sep 17 00:00:00 2001 From: abdesol Date: Fri, 31 Dec 2021 07:00:22 +0300 Subject: [PATCH 011/134] Changed progress text foreground to template binding of the progress bar foreground --- src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml index a8bca77d3e..e971eb46f2 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml @@ -21,7 +21,6 @@ - @@ -30,11 +29,10 @@ + + + - - - - From d8c89df26b850eb57cf29e7b3c993cc2993554eb Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Sat, 1 Jan 2022 10:32:24 +0300 Subject: [PATCH 012/134] [WASM] Cursors --- .../Avalonia.Web.Blazor/AvaloniaView.razor.cs | 1 + src/Web/Avalonia.Web.Blazor/Cursor.cs | 92 +++++++++++++++++++ .../RazorViewTopLevelImpl.cs | 9 +- src/Web/Avalonia.Web.Blazor/WinStubs.cs | 21 ----- .../Avalonia.Web.Blazor/WindowingPlatform.cs | 2 +- 5 files changed, 101 insertions(+), 24 deletions(-) create mode 100644 src/Web/Avalonia.Web.Blazor/Cursor.cs diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index bc2a2f3103..a682a25915 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -257,6 +257,7 @@ namespace Avalonia.Web.Blazor _inputHelper.Hide(); _inputHelper.SetCursor("default"); + _topLevelImpl.SetCssCursor = _inputHelper.SetCursor; Console.WriteLine("starting html canvas setup"); _interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame); diff --git a/src/Web/Avalonia.Web.Blazor/Cursor.cs b/src/Web/Avalonia.Web.Blazor/Cursor.cs new file mode 100644 index 0000000000..cd9010dfc2 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Cursor.cs @@ -0,0 +1,92 @@ +using Avalonia.Input; +using Avalonia.Platform; + +namespace Avalonia.Web.Blazor +{ + public class CssCursor : ICursorImpl + { + public string? Value { get; set; } + + public CssCursor(StandardCursorType type) + { + Value = ToKeyword(type); + } + + /// + /// Create a cursor from base64 image + /// + public CssCursor(string base64, string format, PixelPoint hotspot, StandardCursorType fallback) + { + Value = $"url(data:image/{format},{base64}) {hotspot.X} {hotspot.Y}, {ToKeyword(fallback)}"; + } + + /// + /// Create a cursor from url to *.cur file. + /// + public CssCursor(string url, StandardCursorType fallback) + { + Value = $"url('{url}'), {ToKeyword(fallback)}"; + } + + /// + /// Create a cursor from png/svg and hotspot position + /// + public CssCursor(string url, PixelPoint hotSpot, StandardCursorType fallback) + { + Value = $"url('{url}') {hotSpot.X} {hotSpot.Y}, {ToKeyword(fallback)}"; + } + + private static string ToKeyword(StandardCursorType type) => type switch + { + StandardCursorType.Hand => "pointer", + StandardCursorType.Cross => "crosshair", + StandardCursorType.Help => "help", + StandardCursorType.Ibeam => "text", + StandardCursorType.No => "not-allowed", + StandardCursorType.None => "none", + StandardCursorType.Wait => "progress", + StandardCursorType.AppStarting => "wait", + + StandardCursorType.DragMove => "move", + StandardCursorType.DragCopy => "copy", + StandardCursorType.DragLink => "alias", + + StandardCursorType.UpArrow => "default",/*not found matching one*/ + StandardCursorType.SizeWestEast => "ew-resize", + StandardCursorType.SizeNorthSouth => "ns-resize", + StandardCursorType.SizeAll => "move", + + StandardCursorType.TopSide => "n-resize", + StandardCursorType.BottomSide => "s-resize", + StandardCursorType.LeftSide => "w-resize", + StandardCursorType.RightSide => "e-resize", + StandardCursorType.TopLeftCorner => "nw-resize", + StandardCursorType.TopRightCorner => "ne-resize", + StandardCursorType.BottomLeftCorner => "sw-resize", + StandardCursorType.BottomRightCorner => "se-resize", + + _ => "default", + }; + + public void Dispose() {} + } + + internal class CssCursorFactory : ICursorFactory + { + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + using var imageStream = new MemoryStream(); + cursor.Save(imageStream); + + //not memory optimized because CryptoStream with ToBase64Transform is not supported in the browser. + var base64String = Convert.ToBase64String(imageStream.ToArray()); + return new CssCursor(base64String, "png", hotSpot, StandardCursorType.Arrow); + } + + ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType) + { + return new CssCursor(cursorType); + } + } +} + diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs index acd23a98b0..1b1fca7e26 100644 --- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs @@ -126,8 +126,12 @@ namespace Avalonia.Web.Blazor public void SetCursor(ICursorImpl cursor) { - // nop - + var cur = cursor as CssCursor; + if (cur == null || cur.Value == null) + { + throw new NotSupportedException(); + } + SetCssCursor?.Invoke(cur.Value); } public IPopupImpl? CreatePopup() @@ -146,6 +150,7 @@ namespace Avalonia.Web.Blazor public IEnumerable Surfaces => new object[] { _currentSurface! }; + public Action? SetCssCursor { get; set; } public Action? Input { get; set; } public Action? Paint { get; set; } public Action? Resized { get; set; } diff --git a/src/Web/Avalonia.Web.Blazor/WinStubs.cs b/src/Web/Avalonia.Web.Blazor/WinStubs.cs index 13d83a9ee3..7b2bff6bfd 100644 --- a/src/Web/Avalonia.Web.Blazor/WinStubs.cs +++ b/src/Web/Avalonia.Web.Blazor/WinStubs.cs @@ -23,27 +23,6 @@ namespace Avalonia.Web.Blazor public Task GetDataAsync(string format) => Task.FromResult(new ()); } - internal class CursorStub : ICursorImpl - { - public void Dispose() - { - - } - } - - internal class CursorFactoryStub : ICursorFactory - { - public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) - { - return new CursorStub(); - } - - ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType) - { - return new CursorStub(); - } - } - internal class IconLoaderStub : IPlatformIconLoader { private class IconStub : IWindowIconImpl diff --git a/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs b/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs index 979e66a785..ac970d067f 100644 --- a/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs +++ b/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs @@ -35,7 +35,7 @@ namespace Avalonia.Web.Blazor s_keyboard = new KeyboardDevice(); AvaloniaLocator.CurrentMutable .Bind().ToSingleton() - .Bind().ToSingleton() + .Bind().ToSingleton() .Bind().ToConstant(s_keyboard) .Bind().ToConstant(instance) .Bind().ToConstant(instance) From e9d861bc6659640d9e61797ad8ddc65298d68c14 Mon Sep 17 00:00:00 2001 From: Abdella Solomon <69455299+Abdesol@users.noreply.github.com> Date: Sat, 1 Jan 2022 12:45:54 +0300 Subject: [PATCH 013/134] Changed text foreground of progressbar from template binding to SystemControlForegroundBaseHighBrush --- src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml index e971eb46f2..48b450a338 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml @@ -30,7 +30,7 @@ - + From 100af8ffe307ba7664c3f8c0adfe695f62fab4ce Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sat, 1 Jan 2022 12:46:26 +0300 Subject: [PATCH 014/134] [WASM] fix cursors. works fine --- src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs | 6 ++++-- src/Web/Avalonia.Web.Blazor/Cursor.cs | 5 +++-- src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs | 11 ++++++++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index a682a25915..4b0ada1f27 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -22,6 +22,7 @@ namespace Avalonia.Web.Blazor private DpiWatcherInterop _dpiWatcher = null!; private SKHtmlCanvasInterop.GLInfo? _jsGlInfo = null!; private InputHelperInterop _inputHelper = null!; + private InputHelperInterop _canvasHelper = null!; private ElementReference _htmlCanvas; private ElementReference _inputElement; private double _dpi; @@ -254,10 +255,11 @@ namespace Avalonia.Web.Blazor Threading.Dispatcher.UIThread.Post(async () => { _inputHelper = await InputHelperInterop.ImportAsync(Js, _inputElement); + _canvasHelper = await InputHelperInterop.ImportAsync(Js, _htmlCanvas); _inputHelper.Hide(); - _inputHelper.SetCursor("default"); - _topLevelImpl.SetCssCursor = _inputHelper.SetCursor; + _canvasHelper.SetCursor("default"); + _topLevelImpl.SetCssCursor = _canvasHelper.SetCursor; Console.WriteLine("starting html canvas setup"); _interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame); diff --git a/src/Web/Avalonia.Web.Blazor/Cursor.cs b/src/Web/Avalonia.Web.Blazor/Cursor.cs index cd9010dfc2..d921b2fa6c 100644 --- a/src/Web/Avalonia.Web.Blazor/Cursor.cs +++ b/src/Web/Avalonia.Web.Blazor/Cursor.cs @@ -5,6 +5,7 @@ namespace Avalonia.Web.Blazor { public class CssCursor : ICursorImpl { + public const string Default = "default"; public string? Value { get; set; } public CssCursor(StandardCursorType type) @@ -17,7 +18,7 @@ namespace Avalonia.Web.Blazor /// public CssCursor(string base64, string format, PixelPoint hotspot, StandardCursorType fallback) { - Value = $"url(data:image/{format},{base64}) {hotspot.X} {hotspot.Y}, {ToKeyword(fallback)}"; + Value = $"url(\"data:image/{format};base64,{base64}\") {hotspot.X} {hotspot.Y}, {ToKeyword(fallback)}"; } /// @@ -65,7 +66,7 @@ namespace Avalonia.Web.Blazor StandardCursorType.BottomLeftCorner => "sw-resize", StandardCursorType.BottomRightCorner => "se-resize", - _ => "default", + _ => Default, }; public void Dispose() {} diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs index 1b1fca7e26..ac5f5044d8 100644 --- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs @@ -21,6 +21,7 @@ namespace Avalonia.Web.Blazor private readonly Stopwatch _sw = Stopwatch.StartNew(); private readonly ITextInputMethodImpl _textInputMethod; private readonly TouchDevice _touchDevice; + private string _currentCursor = CssCursor.Default; public RazorViewTopLevelImpl(ITextInputMethodImpl textInputMethod) { @@ -127,11 +128,15 @@ namespace Avalonia.Web.Blazor public void SetCursor(ICursorImpl cursor) { var cur = cursor as CssCursor; - if (cur == null || cur.Value == null) + var val = CssCursor.Default; + if (cur != null && cur.Value != null) { - throw new NotSupportedException(); + val = cur.Value; + } + if (_currentCursor != val) + { + SetCssCursor?.Invoke(val); } - SetCssCursor?.Invoke(cur.Value); } public IPopupImpl? CreatePopup() From e7a36d011fd9b222ad1f762d822e0b74772aecd5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 1 Jan 2022 19:48:16 +0000 Subject: [PATCH 015/134] fix controlcatalog.web --- samples/ControlCatalog.Web/ControlCatalog.Web.csproj | 1 + samples/ControlCatalog/App.xaml.cs | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj index d463dfa84a..199fa85ad2 100644 --- a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj +++ b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj @@ -2,6 +2,7 @@ net6.0 enable + True diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 816356bee9..505f486a6d 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -89,16 +89,18 @@ namespace ControlCatalog if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) { desktopLifetime.MainWindow = new MainWindow(); + + this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() + { + StartupScreenIndex = 1, + }); } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) + { singleViewLifetime.MainView = new MainView(); + } base.OnFrameworkInitializationCompleted(); - - this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() - { - StartupScreenIndex = 1, - }); } } } From c51086401454b08895164525fb3aad99f88ac6bb Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Sun, 2 Jan 2022 05:20:39 +0300 Subject: [PATCH 016/134] [WASM] Fix cursors in macOS, fix default cursor set logic --- src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs | 6 +++++- src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs | 8 ++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index 4b0ada1f27..7644514687 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -259,7 +259,11 @@ namespace Avalonia.Web.Blazor _inputHelper.Hide(); _canvasHelper.SetCursor("default"); - _topLevelImpl.SetCssCursor = _canvasHelper.SetCursor; + _topLevelImpl.SetCssCursor = x => + { + _inputHelper.SetCursor(x);//macOS + _canvasHelper.SetCursor(x);//windows + }; Console.WriteLine("starting html canvas setup"); _interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame); diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs index ac5f5044d8..1d667c0f0c 100644 --- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs @@ -127,15 +127,11 @@ namespace Avalonia.Web.Blazor public void SetCursor(ICursorImpl cursor) { - var cur = cursor as CssCursor; - var val = CssCursor.Default; - if (cur != null && cur.Value != null) - { - val = cur.Value; - } + var val = (cursor as CssCursor)?.Value ?? CssCursor.Default; if (_currentCursor != val) { SetCssCursor?.Invoke(val); + _currentCursor = val; } } From 509b9d8f09eb1928608fe75354a43b6a11e0bf2c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 3 Jan 2022 11:50:43 +0100 Subject: [PATCH 017/134] Add additional null checks to WindowBaseImpl. Fixes #7231. --- src/Avalonia.Native/WindowImplBase.cs | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 4a3baa2788..87b7a7608e 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -28,18 +28,18 @@ namespace Avalonia.Native public string HandleDescriptor => "NSWindow"; - public IntPtr NSView => _native.ObtainNSViewHandle(); + public IntPtr NSView => _native?.ObtainNSViewHandle() ?? IntPtr.Zero; - public IntPtr NSWindow => _native.ObtainNSWindowHandle(); + public IntPtr NSWindow => _native?.ObtainNSWindowHandle() ?? IntPtr.Zero; public IntPtr GetNSViewRetained() { - return _native.ObtainNSViewHandleRetained(); + return _native?.ObtainNSViewHandleRetained() ?? IntPtr.Zero; } public IntPtr GetNSWindowRetained() { - return _native.ObtainNSWindowHandleRetained(); + return _native?.ObtainNSWindowHandleRetained() ?? IntPtr.Zero; } } @@ -260,7 +260,7 @@ namespace Avalonia.Native public void Activate() { - _native.Activate(); + _native?.Activate(); } public bool RawTextInputEvent(uint timeStamp, string text) @@ -322,7 +322,7 @@ namespace Avalonia.Native public void Resize(Size clientSize, PlatformResizeReason reason) { - _native.Resize(clientSize.Width, clientSize.Height, (AvnPlatformResizeReason)reason); + _native?.Resize(clientSize.Width, clientSize.Height, (AvnPlatformResizeReason)reason); } public IRenderer CreateRenderer(IRenderRoot root) @@ -367,14 +367,14 @@ namespace Avalonia.Native public virtual void Show(bool activate, bool isDialog) { - _native.Show(activate.AsComBool(), isDialog.AsComBool()); + _native?.Show(activate.AsComBool(), isDialog.AsComBool()); } public PixelPoint Position { - get => _native.Position.ToAvaloniaPixelPoint(); - set => _native.SetPosition(value.ToAvnPoint()); + get => _native?.Position.ToAvaloniaPixelPoint() ?? default; + set => _native?.SetPosition(value.ToAvnPoint()); } public Point PointToClient(PixelPoint point) @@ -389,12 +389,12 @@ namespace Avalonia.Native public void Hide() { - _native.Hide(); + _native?.Hide(); } public void BeginMoveDrag(PointerPressedEventArgs e) { - _native.BeginMoveDrag(); + _native?.BeginMoveDrag(); } public Size MaxAutoSizeHint => Screen.AllScreens.Select(s => s.Bounds.Size.ToSize(1)) @@ -402,7 +402,7 @@ namespace Avalonia.Native public void SetTopmost(bool value) { - _native.SetTopMost(value.AsComBool()); + _native?.SetTopMost(value.AsComBool()); } public double RenderScaling => _native?.Scaling ?? 1; @@ -438,7 +438,7 @@ namespace Avalonia.Native public void SetMinMaxSize(Size minSize, Size maxSize) { - _native.SetMinMaxSize(minSize.ToAvnSize(), maxSize.ToAvnSize()); + _native?.SetMinMaxSize(minSize.ToAvnSize(), maxSize.ToAvnSize()); } public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) @@ -449,7 +449,7 @@ namespace Avalonia.Native internal void BeginDraggingSession(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard clipboard, IAvnDndResultCallback callback, IntPtr sourceHandle) { - _native.BeginDragAndDropOperation(effects, point, clipboard, callback, sourceHandle); + _native?.BeginDragAndDropOperation(effects, point, clipboard, callback, sourceHandle); } public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) From e6c08a13d70f7173494b83489806e2e1f10b442d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 19:04:06 +0300 Subject: [PATCH 018/134] Added reflection-free API for weak events --- .../NotifyCollectionChangedExtensions.cs | 21 +-- .../Data/Core/IndexerNodeBase.cs | 10 +- .../Core/Plugins/IndeiValidationPlugin.cs | 20 +- .../Plugins/InpcPropertyAccessorPlugin.cs | 24 +-- .../Utilities/IWeakEventSubscriber.cs | 12 ++ src/Avalonia.Base/Utilities/WeakEvent.cs | 175 ++++++++++++++++++ src/Avalonia.Base/Utilities/WeakEvents.cs | 40 ++++ src/Avalonia.Base/Utilities/WeakObservable.cs | 35 +++- .../Utilities/WeakSubscriptionManager.cs | 1 + src/Avalonia.Controls/NativeMenuItem.cs | 10 +- .../Repeater/ItemsRepeater.cs | 32 ++-- src/Avalonia.Controls/TopLevel.cs | 15 +- .../Utils/CollectionChangedEventManager.cs | 15 +- src/Avalonia.Layout/AttachedLayout.cs | 17 ++ .../PropertyInfoAccessorFactory.cs | 36 +--- .../Avalonia.Base.UnitTests/WeakEventTests.cs | 75 ++++++++ 16 files changed, 421 insertions(+), 117 deletions(-) create mode 100644 src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs create mode 100644 src/Avalonia.Base/Utilities/WeakEvent.cs create mode 100644 src/Avalonia.Base/Utilities/WeakEvents.cs create mode 100644 tests/Avalonia.Base.UnitTests/WeakEventTests.cs diff --git a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs index dcd32ddd76..689fcc89a4 100644 --- a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs +++ b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs @@ -59,7 +59,7 @@ namespace Avalonia.Collections } private class WeakCollectionChangedObservable : LightweightObservableBase, - IWeakSubscriber + IWeakEventSubscriber { private WeakReference _sourceReference; @@ -68,31 +68,22 @@ namespace Avalonia.Collections _sourceReference = source; } - public void OnEvent(object? sender, NotifyCollectionChangedEventArgs e) + public void OnEvent(object? sender, + WeakEvent ev, + NotifyCollectionChangedEventArgs e) { PublishNext(e); } - protected override void Initialize() { if (_sourceReference.TryGetTarget(out var instance)) - { - WeakSubscriptionManager.Subscribe( - instance, - nameof(instance.CollectionChanged), - this); - } + WeakEvents.CollectionChanged.Subscribe(instance, this); } protected override void Deinitialize() { if (_sourceReference.TryGetTarget(out var instance)) - { - WeakSubscriptionManager.Unsubscribe( - instance, - nameof(instance.CollectionChanged), - this); - } + WeakEvents.CollectionChanged.Unsubscribe(instance, this); } } } diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index e197e29103..a808827896 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -23,18 +23,16 @@ namespace Avalonia.Data.Core if (incc != null) { - inputs.Add(WeakObservable.FromEventPattern( - incc, - nameof(incc.CollectionChanged)) + inputs.Add(WeakObservable.FromEventPattern( + incc, WeakEvents.CollectionChanged) .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) .Select(_ => GetValue(target))); } if (inpc != null) { - inputs.Add(WeakObservable.FromEventPattern( - inpc, - nameof(inpc.PropertyChanged)) + inputs.Add(WeakObservable.FromEventPattern( + inpc, WeakEvents.PropertyChanged) .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) .Select(_ => GetValue(target))); } diff --git a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs index 9f827daf94..1e7a0d5c8f 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs @@ -11,6 +11,12 @@ namespace Avalonia.Data.Core.Plugins /// public class IndeiValidationPlugin : IDataValidationPlugin { + private static readonly WeakEvent + ErrorsChangedWeakEvent = WeakEvent.Register( + (s, h) => s.ErrorsChanged += h, + (s, h) => s.ErrorsChanged -= h + ); + /// public bool Match(WeakReference reference, string memberName) { @@ -25,7 +31,7 @@ namespace Avalonia.Data.Core.Plugins return new Validator(reference, name, accessor); } - private class Validator : DataValidationBase, IWeakSubscriber + private class Validator : DataValidationBase, IWeakEventSubscriber { private readonly WeakReference _reference; private readonly string _name; @@ -37,7 +43,7 @@ namespace Avalonia.Data.Core.Plugins _name = name; } - void IWeakSubscriber.OnEvent(object? sender, DataErrorsChangedEventArgs e) + void IWeakEventSubscriber.OnEvent(object? notifyDataErrorInfo, WeakEvent ev, DataErrorsChangedEventArgs e) { if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName)) { @@ -51,10 +57,7 @@ namespace Avalonia.Data.Core.Plugins if (target != null) { - WeakSubscriptionManager.Subscribe( - target, - nameof(target.ErrorsChanged), - this); + ErrorsChangedWeakEvent.Subscribe(target, this); } base.SubscribeCore(); @@ -66,10 +69,7 @@ namespace Avalonia.Data.Core.Plugins if (target != null) { - WeakSubscriptionManager.Unsubscribe( - target, - nameof(target.ErrorsChanged), - this); + ErrorsChangedWeakEvent.Unsubscribe(target, this); } base.UnsubscribeCore(); diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index fd532f3014..33cecd10a7 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.ComponentModel; using System.Reflection; using Avalonia.Utilities; @@ -85,7 +86,7 @@ namespace Avalonia.Data.Core.Plugins return found; } - private class Accessor : PropertyAccessorBase, IWeakSubscriber + private class Accessor : PropertyAccessorBase, IWeakEventSubscriber { private readonly WeakReference _reference; private readonly PropertyInfo _property; @@ -129,7 +130,8 @@ namespace Avalonia.Data.Core.Plugins return false; } - void IWeakSubscriber.OnEvent(object? sender, PropertyChangedEventArgs e) + void IWeakEventSubscriber. + OnEvent(object? notifyPropertyChanged, WeakEvent ev, PropertyChangedEventArgs e) { if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) { @@ -148,13 +150,8 @@ namespace Avalonia.Data.Core.Plugins { var inpc = GetReferenceTarget() as INotifyPropertyChanged; - if (inpc != null) - { - WeakSubscriptionManager.Unsubscribe( - inpc, - nameof(inpc.PropertyChanged), - this); - } + if (inpc != null) + WeakEvents.PropertyChanged.Unsubscribe(inpc, this); } private object? GetReferenceTarget() @@ -178,13 +175,8 @@ namespace Avalonia.Data.Core.Plugins { var inpc = GetReferenceTarget() as INotifyPropertyChanged; - if (inpc != null) - { - WeakSubscriptionManager.Subscribe( - inpc, - nameof(inpc.PropertyChanged), - this); - } + if (inpc != null) + WeakEvents.PropertyChanged.Subscribe(inpc, this); } } } diff --git a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs new file mode 100644 index 0000000000..e48c0cb111 --- /dev/null +++ b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs @@ -0,0 +1,12 @@ +using System; + +namespace Avalonia.Utilities; + +/// +/// Defines a listener to a event subscribed vis the . +/// +/// The type of the event arguments. +public interface IWeakEventSubscriber where TEventArgs : EventArgs +{ + void OnEvent(object? sender, WeakEvent ev, TEventArgs e); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs new file mode 100644 index 0000000000..430bc92838 --- /dev/null +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Avalonia.Utilities; + +/// +/// Manages subscriptions to events using weak listeners. +/// +public class WeakEvent : WeakEvent where TEventArgs : EventArgs where TSender : class +{ + private readonly Func, Action> _subscribe; + + readonly ConditionalWeakTable _subscriptions = new(); + + internal WeakEvent( + Action> subscribe, + Action> unsubscribe) + { + _subscribe = (t, s) => + { + subscribe(t, s); + return () => unsubscribe(t, s); + }; + } + + internal WeakEvent(Func, Action> subscribe) + { + _subscribe = subscribe; + } + + public void Subscribe(TSender target, IWeakEventSubscriber subscriber) + { + if (!_subscriptions.TryGetValue(target, out var subscription)) + _subscriptions.Add(target, subscription = new Subscription(this, target)); + subscription.Add(new WeakReference>(subscriber)); + } + + public void Unsubscribe(TSender target, IWeakEventSubscriber subscriber) + { + if (_subscriptions.TryGetValue(target, out var subscription)) + subscription.Remove(subscriber); + } + + private class Subscription + { + private readonly WeakEvent _ev; + private readonly TSender _target; + + private WeakReference>?[] _data = + new WeakReference>[16]; + private int _count; + private readonly Action _unsubscribe; + + public Subscription(WeakEvent ev, TSender target) + { + _ev = ev; + _target = target; + + _unsubscribe = ev._subscribe(target, OnEvent); + } + + void Destroy() + { + _unsubscribe(); + _ev._subscriptions.Remove(_target); + } + + public void Add(WeakReference> s) + { + if (_count == _data.Length) + { + //Extend capacity + var extendedData = new WeakReference>?[_data.Length * 2]; + Array.Copy(_data, extendedData, _data.Length); + _data = extendedData; + } + + _data[_count] = s; + _count++; + } + + public void Remove(IWeakEventSubscriber s) + { + var removed = false; + + for (int c = 0; c < _count; ++c) + { + var reference = _data[c]; + + if (reference != null && reference.TryGetTarget(out var instance) && instance == s) + { + _data[c] = null; + removed = true; + } + } + + if (removed) + { + Compact(); + } + } + + void Compact() + { + int empty = -1; + for (var c = 0; c < _count; c++) + { + var r = _data[c]; + //Mark current index as first empty + if (r == null && empty == -1) + empty = c; + //If current element isn't null and we have an empty one + if (r != null && empty != -1) + { + _data[c] = null; + _data[empty] = r; + empty++; + } + } + + if (empty != -1) + _count = empty; + if (_count == 0) + Destroy(); + } + + void OnEvent(object? sender, TEventArgs eventArgs) + { + var needCompact = false; + for (var c = 0; c < _count; c++) + { + var r = _data[c]; + if (r?.TryGetTarget(out var sub) == true) + sub!.OnEvent(_target, _ev, eventArgs); + else + needCompact = true; + } + + if (needCompact) + Compact(); + } + } + +} + +public class WeakEvent +{ + public static WeakEvent Register( + Action> subscribe, + Action> unsubscribe) where TSender : class where TEventArgs : EventArgs + { + return new WeakEvent(subscribe, unsubscribe); + } + + public static WeakEvent Register( + Func, Action> subscribe) where TSender : class where TEventArgs : EventArgs + { + return new WeakEvent(subscribe); + } + + public static WeakEvent Register( + Action subscribe, + Action unsubscribe) where TSender : class + { + return Register((s, h) => + { + EventHandler handler = (_, e) => h(s, e); + subscribe(s, handler); + return () => unsubscribe(s, handler); + }); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/WeakEvents.cs b/src/Avalonia.Base/Utilities/WeakEvents.cs new file mode 100644 index 0000000000..d1b5e7f12d --- /dev/null +++ b/src/Avalonia.Base/Utilities/WeakEvents.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Windows.Input; + +namespace Avalonia.Utilities; + +public class WeakEvents +{ + /// + /// Represents CollectionChanged event from + /// + public static readonly WeakEvent + CollectionChanged = WeakEvent.Register( + (c, s) => + { + NotifyCollectionChangedEventHandler handler = (_, e) => s(c, e); + c.CollectionChanged += handler; + return () => c.CollectionChanged -= handler; + }); + + /// + /// Represents PropertyChanged event from + /// + public static readonly WeakEvent + PropertyChanged = WeakEvent.Register( + (s, h) => + { + PropertyChangedEventHandler handler = (_, e) => h(s, e); + s.PropertyChanged += handler; + return () => s.PropertyChanged -= handler; + }); + + /// + /// Represents CanExecuteChanged event from + /// + public static readonly WeakEvent CommandCanExecuteChanged = + WeakEvent.Register((s, h) => s.CanExecuteChanged += h, + (s, h) => s.CanExecuteChanged -= h); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/WeakObservable.cs b/src/Avalonia.Base/Utilities/WeakObservable.cs index 52edc7ad1a..6bf1d4082f 100644 --- a/src/Avalonia.Base/Utilities/WeakObservable.cs +++ b/src/Avalonia.Base/Utilities/WeakObservable.cs @@ -18,6 +18,7 @@ namespace Avalonia.Utilities /// Object instance that exposes the event to convert. /// Name of the event to convert. /// + [Obsolete("Use WeakEvent-based overload")] public static IObservable> FromEventPattern( TTarget target, string eventName) @@ -34,7 +35,9 @@ namespace Avalonia.Utilities }).Publish().RefCount(); } - private class Handler : IWeakSubscriber where TEventArgs : EventArgs + private class Handler + : IWeakSubscriber, + IWeakEventSubscriber where TEventArgs : EventArgs { private IObserver> _observer; @@ -47,6 +50,36 @@ namespace Avalonia.Utilities { _observer.OnNext(new EventPattern(sender, e)); } + + public void OnEvent(object? sender, WeakEvent ev, TEventArgs e) + { + _observer.OnNext(new EventPattern(sender, e)); + } } + + /// + /// Converts a WeakEvent conforming to the standard .NET event pattern into an observable + /// sequence, subscribing weakly. + /// + /// The type of target. + /// The type of the event args. + /// Object instance that exposes the event to convert. + /// The weak event to convert. + /// + public static IObservable> FromEventPattern( + TTarget target, WeakEvent ev) + where TEventArgs : EventArgs where TTarget : class + { + _ = target ?? throw new ArgumentNullException(nameof(target)); + _ = ev ?? throw new ArgumentNullException(nameof(ev)); + + return Observable.Create>(observer => + { + var handler = new Handler(observer); + ev.Subscribe(target, handler); + return () => ev.Unsubscribe(target, handler); + }).Publish().RefCount(); + } + } } diff --git a/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs b/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs index 88b1e3c807..dc9e86cc32 100644 --- a/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs +++ b/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs @@ -19,6 +19,7 @@ namespace Avalonia.Utilities /// The event source. /// The name of the event. /// The subscriber. + [Obsolete("Use WeakEvent")] public static void Subscribe(TTarget target, string eventName, IWeakSubscriber subscriber) where TEventArgs : EventArgs { diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 2ceaeb6dba..4d048f0fb0 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -33,7 +33,7 @@ namespace Avalonia.Controls } - class CanExecuteChangedSubscriber : IWeakSubscriber + class CanExecuteChangedSubscriber : IWeakEventSubscriber { private readonly NativeMenuItem _parent; @@ -42,7 +42,7 @@ namespace Avalonia.Controls _parent = parent; } - public void OnEvent(object sender, EventArgs e) + public void OnEvent(object? sender, WeakEvent ev, EventArgs e) { _parent.CanExecuteChanged(); } @@ -160,14 +160,12 @@ namespace Avalonia.Controls set { if (_command != null) - WeakSubscriptionManager.Unsubscribe(_command, - nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber); + WeakEvents.CommandCanExecuteChanged.Unsubscribe(_command, _canExecuteChangedSubscriber); SetAndRaise(CommandProperty, ref _command, value); if (_command != null) - WeakSubscriptionManager.Subscribe(_command, - nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber); + WeakEvents.CommandCanExecuteChanged.Subscribe(_command, _canExecuteChangedSubscriber); CanExecuteChanged(); } diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index ecc0fa3a48..b40cf26df5 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -20,7 +20,7 @@ namespace Avalonia.Controls /// Represents a data-driven collection control that incorporates a flexible layout system, /// custom views, and virtualization. /// - public class ItemsRepeater : Panel, IChildIndexProvider + public class ItemsRepeater : Panel, IChildIndexProvider, IWeakEventSubscriber { /// /// Defines the property. @@ -723,14 +723,8 @@ namespace Avalonia.Controls { oldValue.UninitializeForContext(LayoutContext); - WeakEventHandlerManager.Unsubscribe( - oldValue, - nameof(AttachedLayout.MeasureInvalidated), - InvalidateMeasureForLayout); - WeakEventHandlerManager.Unsubscribe( - oldValue, - nameof(AttachedLayout.ArrangeInvalidated), - InvalidateArrangeForLayout); + AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, this); + AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, this); // Walk through all the elements and make sure they are cleared foreach (var element in Children) @@ -748,14 +742,8 @@ namespace Avalonia.Controls { newValue.InitializeForContext(LayoutContext); - WeakEventHandlerManager.Subscribe( - newValue, - nameof(AttachedLayout.MeasureInvalidated), - InvalidateMeasureForLayout); - WeakEventHandlerManager.Subscribe( - newValue, - nameof(AttachedLayout.ArrangeInvalidated), - InvalidateArrangeForLayout); + AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, this); + AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, this); } bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; @@ -806,9 +794,13 @@ namespace Avalonia.Controls _viewportManager.OnBringIntoViewRequested(e); } - private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateMeasure(); - - private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateArrange(); + void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) + { + if(ev == AttachedLayout.ArrangeInvalidatedWeakEvent) + InvalidateArrange(); + else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent) + InvalidateMeasure(); + } private VirtualizingLayoutContext GetLayoutContext() { diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 5d9a0c8eed..eaee5bdb50 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -34,7 +34,7 @@ namespace Avalonia.Controls IStyleHost, ILogicalRoot, ITextInputMethodRoot, - IWeakSubscriber + IWeakEventSubscriber { /// /// Defines the property. @@ -74,6 +74,12 @@ namespace Avalonia.Controls public static readonly StyledProperty TransparencyBackgroundFallbackProperty = AvaloniaProperty.Register(nameof(TransparencyBackgroundFallback), Brushes.White); + private static readonly WeakEvent + ResourcesChangedWeakEvent = WeakEvent.Register( + (s, h) => s.ResourcesChanged += h, + (s, h) => s.ResourcesChanged -= h + ); + private readonly IInputManager _inputManager; private readonly IAccessKeyHandler _accessKeyHandler; private readonly IKeyboardNavigationHandler _keyboardNavigationHandler; @@ -178,10 +184,7 @@ namespace Avalonia.Controls if (((IStyleHost)this).StylingParent is IResourceHost applicationResources) { - WeakSubscriptionManager.Subscribe( - applicationResources, - nameof(IResourceHost.ResourcesChanged), - this); + ResourcesChangedWeakEvent.Subscribe(applicationResources, this); } impl.LostFocus += PlatformImpl_LostFocus; @@ -286,7 +289,7 @@ namespace Avalonia.Controls /// IMouseDevice IInputRoot.MouseDevice => PlatformImpl?.MouseDevice; - void IWeakSubscriber.OnEvent(object sender, ResourcesChangedEventArgs e) + void IWeakEventSubscriber.OnEvent(object sender, WeakEvent ev, ResourcesChangedEventArgs e) { ((ILogical)this).NotifyResourcesChanged(e); } diff --git a/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs b/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs index 1a190391b7..74705a0262 100644 --- a/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs +++ b/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs @@ -83,7 +83,7 @@ namespace Avalonia.Controls.Utils "Collection listener not registered for this collection/listener combination."); } - private class Entry : IWeakSubscriber, IDisposable + private class Entry : IWeakEventSubscriber, IDisposable { private INotifyCollectionChanged _collection; @@ -91,23 +91,18 @@ namespace Avalonia.Controls.Utils { _collection = collection; Listeners = new List>(); - WeakSubscriptionManager.Subscribe( - _collection, - nameof(INotifyCollectionChanged.CollectionChanged), - this); + WeakEvents.CollectionChanged.Subscribe(_collection, this); } public List> Listeners { get; } public void Dispose() { - WeakSubscriptionManager.Unsubscribe( - _collection, - nameof(INotifyCollectionChanged.CollectionChanged), - this); + WeakEvents.CollectionChanged.Unsubscribe(_collection, this); } - void IWeakSubscriber.OnEvent(object? sender, NotifyCollectionChangedEventArgs e) + void IWeakEventSubscriber. + OnEvent(object? notifyCollectionChanged, WeakEvent ev, NotifyCollectionChangedEventArgs e) { static void Notify( INotifyCollectionChanged incc, diff --git a/src/Avalonia.Layout/AttachedLayout.cs b/src/Avalonia.Layout/AttachedLayout.cs index 6c884641f8..ece8bbe805 100644 --- a/src/Avalonia.Layout/AttachedLayout.cs +++ b/src/Avalonia.Layout/AttachedLayout.cs @@ -4,6 +4,7 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; +using Avalonia.Utilities; namespace Avalonia.Layout { @@ -19,10 +20,26 @@ namespace Avalonia.Layout /// public event EventHandler? MeasureInvalidated; + /// + /// Occurs when the measurement state (layout) has been invalidated. + /// + public static readonly WeakEvent MeasureInvalidatedWeakEvent = + WeakEvent.Register( + (s, h) => s.MeasureInvalidated += h, + (s, h) => s.MeasureInvalidated -= h); + /// /// Occurs when the arrange state (layout) has been invalidated. /// public event EventHandler? ArrangeInvalidated; + + /// + /// Occurs when the arrange state (layout) has been invalidated. + /// + public static readonly WeakEvent ArrangeInvalidatedWeakEvent = + WeakEvent.Register( + (s, h) => s.ArrangeInvalidated += h, + (s, h) => s.ArrangeInvalidated -= h); /// /// Initializes any per-container state the layout requires when it is attached to an diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs index b3f78bfbe3..c21a2d4299 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs @@ -72,7 +72,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings } } - internal class InpcPropertyAccessor : PropertyAccessorBase + internal class InpcPropertyAccessor : PropertyAccessorBase, IWeakEventSubscriber { protected readonly WeakReference _reference; private readonly IPropertyInfo _property; @@ -110,7 +110,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings return false; } - void OnNotifyPropertyChanged(object sender, PropertyChangedEventArgs e) + public void OnEvent(object sender, WeakEvent ev, PropertyChangedEventArgs e) { if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) { @@ -128,10 +128,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) { - WeakEventHandlerManager.Unsubscribe( - inpc, - nameof(INotifyPropertyChanged.PropertyChanged), - OnNotifyPropertyChanged); + WeakEvents.PropertyChanged.Unsubscribe(inpc, this); } } @@ -148,16 +145,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings private void SubscribeToChanges() { if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) - { - WeakEventHandlerManager.Subscribe( - inpc, - nameof(INotifyPropertyChanged.PropertyChanged), - OnNotifyPropertyChanged); - } + WeakEvents.PropertyChanged.Subscribe(inpc, this); } } - internal class IndexerAccessor : InpcPropertyAccessor + internal class IndexerAccessor : InpcPropertyAccessor, IWeakEventSubscriber { private int _index; @@ -172,27 +164,17 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { base.SubscribeCore(); if (_reference.TryGetTarget(out var o) && o is INotifyCollectionChanged incc) - { - WeakEventHandlerManager.Subscribe( - incc, - nameof(INotifyCollectionChanged.CollectionChanged), - OnNotifyCollectionChanged); - } + WeakEvents.CollectionChanged.Subscribe(incc, this); } protected override void UnsubscribeCore() { base.UnsubscribeCore(); if (_reference.TryGetTarget(out var o) && o is INotifyCollectionChanged incc) - { - WeakEventHandlerManager.Unsubscribe( - incc, - nameof(INotifyCollectionChanged.CollectionChanged), - OnNotifyCollectionChanged); - } + WeakEvents.CollectionChanged.Unsubscribe(incc, this); } - - void OnNotifyCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) + + public void OnEvent(object? sender, WeakEvent ev, NotifyCollectionChangedEventArgs args) { if (ShouldNotifyListeners(args)) { diff --git a/tests/Avalonia.Base.UnitTests/WeakEventTests.cs b/tests/Avalonia.Base.UnitTests/WeakEventTests.cs new file mode 100644 index 0000000000..81009f7c5f --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/WeakEventTests.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Base.UnitTests +{ + public class WeakEventManagerTests + { + class EventSource + { + public event EventHandler Event; + + public void Fire() + { + Event?.Invoke(this, new EventArgs()); + } + + public static readonly WeakEvent WeakEvent = WeakEvent.Register( + (t, s) => t.Event += s, + (t, s) => t.Event -= s); + } + + class Subscriber : IWeakEventSubscriber + { + private readonly Action _onEvent; + + public Subscriber(Action onEvent) + { + _onEvent = onEvent; + } + + public void OnEvent(object sender, WeakEvent ev, EventArgs args) + { + _onEvent?.Invoke(); + } + } + + [Fact] + public void EventShouldBePassedToSubscriber() + { + bool handled = false; + var subscriber = new Subscriber(() => handled = true); + var source = new EventSource(); + EventSource.WeakEvent.Subscribe(source, subscriber); + + source.Fire(); + Assert.True(handled); + } + + + [Fact] + public void EventHandlerShouldNotBeKeptAlive() + { + bool handled = false; + var source = new EventSource(); + AddSubscriber(source, () => handled = true); + for (int c = 0; c < 10; c++) + { + GC.Collect(); + GC.Collect(3, GCCollectionMode.Forced, true); + } + source.Fire(); + Assert.False(handled); + } + + private void AddSubscriber(EventSource source, Action func) + { + EventSource.WeakEvent.Subscribe(source, new Subscriber(func)); + } + } +} From c2d627360cc256e503c2d78810dffd380af4b781 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Mon, 3 Jan 2022 19:15:45 +0300 Subject: [PATCH 019/134] [MacOS] fix quit menu item copy paste mistake --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index bda1c91750..5015c92e46 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -131,7 +131,7 @@ namespace Avalonia.Native }; quitItem.Click += (sender, args) => { - _applicationCommands.ShowAll(); + (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.Shutdown(); }; result.Add(quitItem); } From 1221356df31623fd2cb036c8194ef18ee780012f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 19:20:44 +0300 Subject: [PATCH 020/134] apicompat --- src/Avalonia.Controls/ApiCompatBaseline.txt | 7 ++++++- src/Avalonia.Dialogs/ApiCompatBaseline.txt | 3 +++ tests/Avalonia.Base.UnitTests/WeakEventTests.cs | 10 +++++----- 3 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 src/Avalonia.Dialogs/ApiCompatBaseline.txt diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index dd41c30e85..9b7d37e108 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -29,15 +29,20 @@ MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDownValueChang MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.NewValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.TopLevel' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Window' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.WindowBase' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Embedding.EmbeddableControlRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. MembersMustExist : Member 'public System.Action Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.Platform.ITopLevelNativeMenuExporter.SetNativeMenu(Avalonia.Controls.NativeMenu)' is present in the contract but not in the implementation. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Primitives.PopupRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. @@ -57,4 +62,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract. -Total Issues: 58 +Total Issues: 63 diff --git a/src/Avalonia.Dialogs/ApiCompatBaseline.txt b/src/Avalonia.Dialogs/ApiCompatBaseline.txt new file mode 100644 index 0000000000..9cb1b47015 --- /dev/null +++ b/src/Avalonia.Dialogs/ApiCompatBaseline.txt @@ -0,0 +1,3 @@ +Compat issues with assembly Avalonia.Dialogs: +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Dialogs.AboutAvaloniaDialog' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. +Total Issues: 1 diff --git a/tests/Avalonia.Base.UnitTests/WeakEventTests.cs b/tests/Avalonia.Base.UnitTests/WeakEventTests.cs index 81009f7c5f..2663b4858f 100644 --- a/tests/Avalonia.Base.UnitTests/WeakEventTests.cs +++ b/tests/Avalonia.Base.UnitTests/WeakEventTests.cs @@ -8,18 +8,18 @@ using Xunit; namespace Avalonia.Base.UnitTests { - public class WeakEventManagerTests + public class WeakEventTests { class EventSource { - public event EventHandler Event; + public event EventHandler Event; public void Fire() { Event?.Invoke(this, new EventArgs()); } - public static readonly WeakEvent WeakEvent = WeakEvent.Register( + public static readonly WeakEvent WeakEv = WeakEvent.Register( (t, s) => t.Event += s, (t, s) => t.Event -= s); } @@ -45,7 +45,7 @@ namespace Avalonia.Base.UnitTests bool handled = false; var subscriber = new Subscriber(() => handled = true); var source = new EventSource(); - EventSource.WeakEvent.Subscribe(source, subscriber); + EventSource.WeakEv.Subscribe(source, subscriber); source.Fire(); Assert.True(handled); @@ -69,7 +69,7 @@ namespace Avalonia.Base.UnitTests private void AddSubscriber(EventSource source, Action func) { - EventSource.WeakEvent.Subscribe(source, new Subscriber(func)); + EventSource.WeakEv.Subscribe(source, new Subscriber(func)); } } } From 059e367cbfa9026dcfdf8f239d28cbce44cf1ed0 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 19:24:31 +0300 Subject: [PATCH 021/134] Schedule weak event list Compact to be called later with Background dispatcher priority --- src/Avalonia.Base/Utilities/WeakEvent.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index 430bc92838..c353e11263 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using Avalonia.Threading; namespace Avalonia.Utilities; @@ -53,6 +54,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event new WeakReference>[16]; private int _count; private readonly Action _unsubscribe; + private bool _compactScheduled; public Subscription(WeakEvent ev, TSender target) { @@ -99,12 +101,21 @@ public class WeakEvent : WeakEvent where TEventArgs : Event if (removed) { - Compact(); + ScheduleCompact(); } } + void ScheduleCompact() + { + if(_compactScheduled) + return; + _compactScheduled = true; + Dispatcher.UIThread.Post(Compact, DispatcherPriority.Background); + } + void Compact() { + _compactScheduled = false; int empty = -1; for (var c = 0; c < _count; c++) { @@ -140,7 +151,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event } if (needCompact) - Compact(); + ScheduleCompact(); } } From f010322a066b570eb1d979c4b1013934575149ed Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 19:50:22 +0300 Subject: [PATCH 022/134] Force compacting WeakEvent subscriber list in before assertions in tests --- .../ExpressionObserverTests_DataValidation.cs | 3 +++ .../Core/ExpressionObserverTests_Indexer.cs | 12 +++++++++ .../ExpressionObserverTests_Observable.cs | 11 +++++++- .../Core/ExpressionObserverTests_Property.cs | 27 +++++++++++++++++-- .../Plugins/IndeiValidationPluginTests.cs | 3 +++ 5 files changed, 53 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs index ecc43aa3a5..43192584af 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs @@ -6,6 +6,7 @@ using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Markup.Parsers; +using Avalonia.Threading; using Avalonia.UnitTests; using Xunit; @@ -67,6 +68,8 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.Equal(1, data.ErrorsChangedSubscriptionCount); sub.Dispose(); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, data.ErrorsChangedSubscriptionCount); } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs index 6289ec46c7..20a4cb6d98 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs @@ -9,6 +9,7 @@ using Avalonia.Data.Core; using Avalonia.UnitTests; using Xunit; using Avalonia.Markup.Parsers; +using Avalonia.Threading; namespace Avalonia.Base.UnitTests.Data.Core { @@ -110,6 +111,9 @@ namespace Avalonia.Base.UnitTests.Data.Core data.Foo.Add("baz"); } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "baz" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); @@ -127,6 +131,8 @@ namespace Avalonia.Base.UnitTests.Data.Core { data.Foo.RemoveAt(0); } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(new[] { "foo", "bar" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); @@ -145,6 +151,9 @@ namespace Avalonia.Base.UnitTests.Data.Core { data.Foo[1] = "baz"; } + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(new[] { "bar", "baz" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); @@ -202,6 +211,9 @@ namespace Avalonia.Base.UnitTests.Data.Core data.Foo["foo"] = "bar2"; } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + var expected = new[] { "bar", "bar2" }; Assert.Equal(expected, result); Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs index a70d4574a6..4f88d2de7c 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs @@ -5,6 +5,7 @@ using System.Reactive.Subjects; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Markup.Parsers; +using Avalonia.Threading; using Avalonia.UnitTests; using Xunit; @@ -68,6 +69,8 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.Equal(new[] { "foo" }, result); sub.Dispose(); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); GC.KeepAlive(data); @@ -109,10 +112,16 @@ namespace Avalonia.Base.UnitTests.Data.Core var sub = target.Subscribe(x => result.Add(x)); data1.Next.OnNext(data2); sync.ExecutePostedCallbacks(); - + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(new[] { new BindingNotification("foo") }, result); sub.Dispose(); + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, data1.PropertyChangedSubscriptionCount); GC.KeepAlive(data1); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index 32cdd21e04..a9c62a3c4a 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -10,6 +10,7 @@ using Avalonia.UnitTests; using Xunit; using System.Threading.Tasks; using Avalonia.Markup.Parsers; +using Avalonia.Threading; namespace Avalonia.Base.UnitTests.Data.Core { @@ -182,6 +183,9 @@ namespace Avalonia.Base.UnitTests.Data.Core sub.Dispose(); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); GC.KeepAlive(data); @@ -209,8 +213,11 @@ namespace Avalonia.Base.UnitTests.Data.Core data.RaisePropertyChanged(null); Assert.Equal(new[] { "foo", "bar", "bar" }, result); - + sub.Dispose(); + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); @@ -231,7 +238,9 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.Equal(new[] { "bar", "baz", null }, result); sub.Dispose(); - + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); @@ -253,6 +262,9 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.Equal(new[] { "bar", "baz", null }, result); sub.Dispose(); + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); @@ -297,6 +309,9 @@ namespace Avalonia.Base.UnitTests.Data.Core sub.Dispose(); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); Assert.Equal(0, old.PropertyChangedSubscriptionCount); @@ -329,6 +344,9 @@ namespace Avalonia.Base.UnitTests.Data.Core result); sub.Dispose(); + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); @@ -412,6 +430,9 @@ namespace Avalonia.Base.UnitTests.Data.Core sub1.Dispose(); sub2.Dispose(); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); GC.KeepAlive(data); @@ -535,6 +556,8 @@ namespace Avalonia.Base.UnitTests.Data.Core }, result); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, first.PropertyChangedSubscriptionCount); Assert.Equal(0, second.PropertyChangedSubscriptionCount); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs index d8eddf6330..e8f1f38b90 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using Avalonia.Data; using Avalonia.Data.Core.Plugins; +using Avalonia.Threading; using Xunit; namespace Avalonia.Base.UnitTests.Data.Core.Plugins @@ -57,6 +58,8 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins validator.Subscribe(_ => { }); Assert.Equal(1, data.ErrorsChangedSubscriptionCount); validator.Unsubscribe(); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, data.ErrorsChangedSubscriptionCount); } From b6219db4ad844f7e461fbcabcfa07d00d39c0d92 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 20:44:37 +0300 Subject: [PATCH 023/134] More Dispatcher.UIThread.RunJobs in tests --- .../Data/BindingTests.cs | 7 +++++++ .../ExpressionObserverBuilderTests_Indexer.cs | 15 ++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index a0dd565a87..055de999e2 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -12,6 +12,7 @@ using System.Runtime.CompilerServices; using Avalonia.UnitTests; using Avalonia.Data.Converters; using Avalonia.Data.Core; +using Avalonia.Threading; namespace Avalonia.Markup.UnitTests.Data { @@ -160,6 +161,9 @@ namespace Avalonia.Markup.UnitTests.Data target.Bind(TextBlock.TextProperty, new Binding("Foo", BindingMode.OneTime)); target.DataContext = source; + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, source.SubscriberCount); } @@ -608,6 +612,9 @@ namespace Avalonia.Markup.UnitTests.Data root.DataContext = source; } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, source.SubscriberCount); } diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs index 39d6152b69..dbf6ef2ce9 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs @@ -10,6 +10,7 @@ using System.Collections.ObjectModel; using System.Reactive.Linq; using System.Text; using System.Threading.Tasks; +using Avalonia.Threading; using Xunit; namespace Avalonia.Markup.UnitTests.Parsers @@ -159,7 +160,10 @@ namespace Avalonia.Markup.UnitTests.Parsers { data.Foo.Add("baz"); } - + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "baz" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); @@ -178,6 +182,9 @@ namespace Avalonia.Markup.UnitTests.Parsers data.Foo.RemoveAt(0); } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(new[] { "foo", "bar" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); @@ -196,6 +203,9 @@ namespace Avalonia.Markup.UnitTests.Parsers data.Foo[1] = "baz"; } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(new[] { "bar", "baz" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); @@ -252,6 +262,9 @@ namespace Avalonia.Markup.UnitTests.Parsers data.Foo["foo"] = "bar2"; } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + var expected = new[] { "bar", "bar2" }; Assert.Equal(expected, result); Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount); From e516fe2e6043cce99926981dfdfc52fe09379878 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 20:45:55 +0300 Subject: [PATCH 024/134] More Dispatcher.UIThread.RunJobjs() in tests --- tests/Avalonia.LeakTests/AvaloniaObjectTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs b/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs index 54f9a87f94..19208b15f3 100644 --- a/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs +++ b/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Subjects; +using Avalonia.Threading; using JetBrains.dotMemoryUnit; using Xunit; using Xunit.Abstractions; @@ -56,7 +57,9 @@ namespace Avalonia.LeakTests completeSource(); GC.Collect(); - + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + GC.Collect(); Assert.False(weakSource.IsAlive); } From a175fbbe3ad2ad82be446925760baf12b9c65ce7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 3 Jan 2022 19:06:39 +0100 Subject: [PATCH 025/134] Check for null visual parent. (#7298) It shouldn't be possible to come across a null visual parent here because the visual should be attached to the visual tree, but according to #6930, it is. Because we don't have a repro here, defensively return a null value instead of throwing if we hit this case. --- src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index cb916293ac..63c22efc3f 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -126,7 +126,12 @@ namespace Avalonia.Rendering.SceneGraph while (node == null && visual.IsVisible) { - visual = visual.VisualParent!; + var parent = visual.VisualParent; + + if (parent is null) + return null; + + visual = parent; node = scene.FindNode(visual); } From 32bf6160dfcb3c0a0550e627a51325b2f935625d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 3 Jan 2022 18:23:40 +0000 Subject: [PATCH 026/134] use IControlledApplicationLifetime --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 5015c92e46..57a8701202 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -131,7 +131,7 @@ namespace Avalonia.Native }; quitItem.Click += (sender, args) => { - (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.Shutdown(); + (Application.Current.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown(); }; result.Add(quitItem); } From dfd0523e36e3f5227305a6da73cd61d6a0943b93 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 22:12:51 +0300 Subject: [PATCH 027/134] Optimize Pen's subscriptions to IAffects render --- src/Avalonia.Visuals/ApiCompatBaseline.txt | 5 +- src/Avalonia.Visuals/Media/Pen.cs | 110 ++++++++------------- 2 files changed, 46 insertions(+), 69 deletions(-) diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index dcb3246a63..68e3673cfe 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -7,6 +7,9 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Task MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.PageSlide.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.GlyphRun..ctor()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphTypeface.set(Avalonia.Media.GlyphTypeface)' does not exist in the implementation but it does exist in the contract. +CannotSealType : Type 'Avalonia.Media.Pen' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +MembersMustExist : Member 'protected void Avalonia.Media.Pen.AffectsRender(Avalonia.AvaloniaProperty[])' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Media.Pen.RaiseInvalidated(System.EventArgs)' does not exist in the implementation but it does exist in the contract. TypeCannotChangeClassification : Type 'Avalonia.Media.Immutable.ImmutableSolidColorBrush' is a 'class' in the implementation but is a 'struct' in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract. @@ -83,4 +86,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avaloni InterfacesShouldHaveSameMembers : Interface member 'public System.TimeSpan Avalonia.Platform.IPlatformSettings.TouchDoubleClickTime' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avalonia.Platform.IPlatformSettings.TouchDoubleClickSize.get()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.TimeSpan Avalonia.Platform.IPlatformSettings.TouchDoubleClickTime.get()' is present in the implementation but not in the contract. -Total Issues: 84 +Total Issues: 87 diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index 7c966a35cf..f4ae58eff3 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -7,7 +7,7 @@ namespace Avalonia.Media /// /// Describes how a stroke is drawn. /// - public class Pen : AvaloniaObject, IPen + public sealed class Pen : AvaloniaObject, IPen, IWeakEventSubscriber { /// /// Defines the property. @@ -45,6 +45,9 @@ namespace Avalonia.Media public static readonly StyledProperty MiterLimitProperty = AvaloniaProperty.Register(nameof(MiterLimit), 10.0); + private EventHandler? _invalidated; + private IAffectsRender? _subscribedTo; + /// /// Initializes a new instance of the class. /// @@ -96,17 +99,6 @@ namespace Avalonia.Media DashStyle = dashStyle; } - static Pen() - { - AffectsRender( - BrushProperty, - ThicknessProperty, - DashStyleProperty, - LineCapProperty, - LineJoinProperty, - MiterLimitProperty); - } - /// /// Gets or sets the brush used to draw the stroke. /// @@ -116,6 +108,11 @@ namespace Avalonia.Media set => SetValue(BrushProperty, value); } + private static readonly WeakEvent InvalidatedWeakEvent = + WeakEvent.Register( + (s, h) => s.Invalidated += h, + (s, h) => s.Invalidated -= h); + /// /// Gets or sets the stroke thickness. /// @@ -165,7 +162,19 @@ namespace Avalonia.Media /// /// Raised when the pen changes. /// - public event EventHandler? Invalidated; + public event EventHandler? Invalidated + { + add + { + _invalidated += value; + UpdateBrushSubscription(); + } + remove + { + _invalidated -= value; + UpdateBrushSubscription(); + } + } /// /// Creates an immutable clone of the brush. @@ -182,68 +191,33 @@ namespace Avalonia.Media MiterLimit); } - /// - /// Marks a property as affecting the pen's visual representation. - /// - /// The properties. - /// - /// After a call to this method in a pen's static constructor, any change to the - /// property will cause the event to be raised on the pen. - /// - protected static void AffectsRender(params AvaloniaProperty[] properties) - where T : Pen + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - static void Invalidate(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is T sender) - { - sender.RaiseInvalidated(EventArgs.Empty); - } - } + _invalidated?.Invoke(this, EventArgs.Empty); + if(change.Property == BrushProperty) + UpdateBrushSubscription(); + base.OnPropertyChanged(change); + } - static void InvalidateAndSubscribe(AvaloniaPropertyChangedEventArgs e) + void UpdateBrushSubscription() + { + if ((_invalidated == null || _subscribedTo != Brush) && _subscribedTo != null) { - if (e.Sender is T sender) - { - if (e.OldValue is IAffectsRender oldValue) - { - WeakEventHandlerManager.Unsubscribe( - oldValue, - nameof(oldValue.Invalidated), - sender.AffectsRenderInvalidated); - } - - if (e.NewValue is IAffectsRender newValue) - { - WeakEventHandlerManager.Subscribe( - newValue, - nameof(newValue.Invalidated), - sender.AffectsRenderInvalidated); - } - - sender.RaiseInvalidated(EventArgs.Empty); - } + InvalidatedWeakEvent.Unsubscribe(_subscribedTo, this); + _subscribedTo = null; } - foreach (var property in properties) + if (_invalidated != null && _subscribedTo != Brush && Brush is IAffectsRender affectsRender) { - if (property.CanValueAffectRender()) - { - property.Changed.Subscribe(e => InvalidateAndSubscribe(e)); - } - else - { - property.Changed.Subscribe(e => Invalidate(e)); - } + InvalidatedWeakEvent.Subscribe(affectsRender, this); + _subscribedTo = affectsRender; } } - - /// - /// Raises the event. - /// - /// The event args. - protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); - - private void AffectsRenderInvalidated(object? sender, EventArgs e) => RaiseInvalidated(EventArgs.Empty); + + void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) + { + if (ev == InvalidatedWeakEvent) + _invalidated?.Invoke(this, EventArgs.Empty); + } } } From 7e45ee1e537e5f0b67c23cba159638dfcf5241a7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 4 Jan 2022 01:19:14 +0300 Subject: [PATCH 028/134] Subscribe to DashStyle too --- src/Avalonia.Visuals/Media/Pen.cs | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index f4ae58eff3..65ba851100 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -46,7 +46,8 @@ namespace Avalonia.Media AvaloniaProperty.Register(nameof(MiterLimit), 10.0); private EventHandler? _invalidated; - private IAffectsRender? _subscribedTo; + private IAffectsRender? _subscribedToBrush; + private IAffectsRender? _subscribedToDashes; /// /// Initializes a new instance of the class. @@ -167,12 +168,12 @@ namespace Avalonia.Media add { _invalidated += value; - UpdateBrushSubscription(); + UpdateSubscriptions(); } remove { _invalidated -= value; - UpdateBrushSubscription(); + UpdateSubscriptions(); } } @@ -195,24 +196,33 @@ namespace Avalonia.Media { _invalidated?.Invoke(this, EventArgs.Empty); if(change.Property == BrushProperty) - UpdateBrushSubscription(); + UpdateSubscription(ref _subscribedToBrush, Brush); + if(change.Property == DashStyleProperty) + UpdateSubscription(ref _subscribedToDashes, DashStyle); base.OnPropertyChanged(change); } - void UpdateBrushSubscription() + + void UpdateSubscription(ref IAffectsRender? field, object? value) { - if ((_invalidated == null || _subscribedTo != Brush) && _subscribedTo != null) + if ((_invalidated == null || field != value) && field != null) { - InvalidatedWeakEvent.Unsubscribe(_subscribedTo, this); - _subscribedTo = null; + InvalidatedWeakEvent.Unsubscribe(field, this); + field = null; } - if (_invalidated != null && _subscribedTo != Brush && Brush is IAffectsRender affectsRender) + if (_invalidated != null && field != value && value is IAffectsRender affectsRender) { InvalidatedWeakEvent.Subscribe(affectsRender, this); - _subscribedTo = affectsRender; + field = affectsRender; } } + + void UpdateSubscriptions() + { + UpdateSubscription(ref _subscribedToBrush, Brush); + UpdateSubscription(ref _subscribedToDashes, DashStyle); + } void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) { From a08c4888e0e2e1112275d228b42a0a3899e2a5f2 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 4 Jan 2022 18:01:32 +0300 Subject: [PATCH 029/134] WeakEvent: cache Compact() delegate --- src/Avalonia.Base/Utilities/WeakEvent.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index c353e11263..0b32015a8a 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -49,6 +49,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { private readonly WeakEvent _ev; private readonly TSender _target; + private readonly Action _compact; private WeakReference>?[] _data = new WeakReference>[16]; @@ -60,7 +61,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { _ev = ev; _target = target; - + _compact = Compact; _unsubscribe = ev._subscribe(target, OnEvent); } @@ -110,7 +111,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event if(_compactScheduled) return; _compactScheduled = true; - Dispatcher.UIThread.Post(Compact, DispatcherPriority.Background); + Dispatcher.UIThread.Post(_compact, DispatcherPriority.Background); } void Compact() From c73a6c86c0d0fba2b969a81dd9459ce0d0d02125 Mon Sep 17 00:00:00 2001 From: odalet Date: Tue, 4 Jan 2022 17:35:59 +0100 Subject: [PATCH 030/134] Fixes #7309 - Each time we retrieve a null *PlatformOptions from AvaloniaLocator, return a default instance --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 8 ++++---- src/Avalonia.Native/AvaloniaNativePlatform.cs | 4 ++-- src/Avalonia.X11/Glx/GlxDisplay.cs | 4 ++-- .../LinuxFramebufferPlatform.cs | 4 ++-- src/Windows/Avalonia.Win32/Win32GlManager.cs | 9 ++++----- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 57a8701202..40ffc31728 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -80,8 +80,8 @@ namespace Avalonia.Native }; result.Add(aboutItem); - var macOpts = AvaloniaLocator.Current.GetService(); - if (macOpts == null || !macOpts.DisableDefaultApplicationMenuItems) + var macOpts = AvaloniaLocator.Current.GetService() ?? new MacOSPlatformOptions(); + if (!macOpts.DisableDefaultApplicationMenuItems) { result.Add(new NativeMenuItemSeparator()); @@ -142,9 +142,9 @@ namespace Avalonia.Native private void DoLayoutReset(bool forceUpdate = false) { - var macOpts = AvaloniaLocator.Current.GetService(); + var macOpts = AvaloniaLocator.Current.GetService() ?? new MacOSPlatformOptions(); - if (macOpts != null && macOpts.DisableNativeMenus) + if (macOpts.DisableNativeMenus) { return; } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 5fa50f0e7f..1eadf70b13 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -96,9 +96,9 @@ namespace Avalonia.Native _factory.Initialize(new GCHandleDeallocator(), applicationPlatform); if (_factory.MacOptions != null) { - var macOpts = AvaloniaLocator.Current.GetService(); + var macOpts = AvaloniaLocator.Current.GetService() ?? new MacOSPlatformOptions(); - _factory.MacOptions.SetShowInDock(macOpts?.ShowInDock != false ? 1 : 0); + _factory.MacOptions.SetShowInDock(macOpts.ShowInDock ? 1 : 0); } AvaloniaLocator.CurrentMutable diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index fa8c866c09..fcdc10e999 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -95,8 +95,8 @@ namespace Avalonia.X11.Glx if (Environment.GetEnvironmentVariable("AVALONIA_GLX_IGNORE_RENDERER_BLACKLIST") != "1") { - var blacklist = AvaloniaLocator.Current.GetService() - ?.GlxRendererBlacklist; + var opts = AvaloniaLocator.Current.GetService() ?? new X11PlatformOptions(); + var blacklist = opts.GlxRendererBlacklist; if (blacklist != null) foreach (var item in blacklist) if (glInterface.Renderer.Contains(item)) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index f4db6bf48a..4add4c423b 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -38,11 +38,11 @@ namespace Avalonia.LinuxFramebuffer if (_fb is IGlOutputBackend gl) AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl.PlatformOpenGlInterface); - var opts = AvaloniaLocator.Current.GetService(); + var opts = AvaloniaLocator.Current.GetService() ?? new LinuxFramebufferPlatformOptions(); AvaloniaLocator.CurrentMutable .Bind().ToConstant(Threading) - .Bind().ToConstant(new DefaultRenderTimer(opts?.Fps ?? 60)) + .Bind().ToConstant(new DefaultRenderTimer(opts.Fps)) .Bind().ToConstant(new RenderLoop()) .Bind().ToTransient() .Bind().ToConstant(new KeyboardDevice()) diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index 289c100d51..0376a41f8c 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -13,19 +13,18 @@ namespace Avalonia.Win32 { AvaloniaLocator.CurrentMutable.Bind().ToLazy(() => { - var opts = AvaloniaLocator.Current.GetService(); - if (opts?.UseWgl == true) + var opts = AvaloniaLocator.Current.GetService() ?? new Win32PlatformOptions(); + if (opts.UseWgl) { var wgl = WglPlatformOpenGlInterface.TryCreate(); return wgl; } - if (opts?.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7) + if (opts.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7) { var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); - if (egl != null && - opts?.UseWindowsUIComposition == true) + if (egl != null && opts.UseWindowsUIComposition) { WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); } From 1604be478470c2dffd1c41055ea854fc89fa6b87 Mon Sep 17 00:00:00 2001 From: Tim U Date: Wed, 5 Jan 2022 08:00:03 +0100 Subject: [PATCH 031/134] fix Validation for SelectedDateProperty not working --- src/Avalonia.Controls/Calendar/CalendarDatePicker.cs | 11 +++++------ .../Controls/CalendarDatePicker.xaml | 6 +++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs index a856ee071c..cd9c80d3e0 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs @@ -185,7 +185,8 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect( nameof(SelectedDate), o => o.SelectedDate, - (o, v) => o.SelectedDate = v); + (o, v) => o.SelectedDate = v, + enableDataValidation: true); public static readonly StyledProperty SelectedDateFormatProperty = AvaloniaProperty.Register( @@ -533,13 +534,11 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { - base.OnPropertyChanged(change); - - if (change.Property == SelectedDateProperty) + if (property == SelectedDateProperty) { - DataValidationErrors.SetError(this, change.NewValue.Error); + DataValidationErrors.SetError(this, value.Error); } } diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml index 6c4e94caf1..26c3bbc19f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml @@ -33,6 +33,7 @@ + @@ -107,7 +108,6 @@ Padding="{TemplateBinding Padding}" Watermark="{TemplateBinding Watermark}" UseFloatingWatermark="{TemplateBinding UseFloatingWatermark}" - DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" Grid.Column="0"/> @@ -136,8 +136,12 @@ DisplayDateEnd="{TemplateBinding DisplayDateEnd}" /> + + From fe21e298afbff5ed03fa0b08c95dad5e2352dd25 Mon Sep 17 00:00:00 2001 From: Tim U Date: Wed, 5 Jan 2022 08:00:43 +0100 Subject: [PATCH 032/134] Show validation in Demo App --- .../Pages/CalendarDatePickerPage.xaml | 5 +++++ .../ViewModels/MainWindowViewModel.cs | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml index 3e50bf8a08..2fe16ba8e3 100644 --- a/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml +++ b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml @@ -1,5 +1,7 @@ A control for selecting dates with a calendar drop-down @@ -39,6 +41,9 @@ + + + diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 4b3cfa9c9d..2b0c30f311 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Notifications; using Avalonia.Dialogs; using Avalonia.Platform; using System; +using System.ComponentModel.DataAnnotations; using MiniMvvm; namespace ControlCatalog.ViewModels @@ -164,5 +165,17 @@ namespace ControlCatalog.ViewModels public MiniCommand ExitCommand { get; } public MiniCommand ToggleMenuItemCheckedCommand { get; } + + private DateTime? _validatedDateExample; + + /// + /// A required DateTime which should demonstrate validation for the DateTimePicker + /// + [Required] + public DateTime? ValidatedDateExample + { + get => _validatedDateExample; + set => this.RaiseAndSetIfChanged(ref _validatedDateExample, value); + } } } From 88db01532f70f209af9a1cb90f8bd13ad36fc6bf Mon Sep 17 00:00:00 2001 From: Tim U Date: Tue, 4 Jan 2022 16:34:41 +0100 Subject: [PATCH 033/134] Implement CellEditingTemplate --- .../DataGridTemplateColumn.cs | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs index 7e95dd100c..fbdad0a8ad 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs @@ -30,6 +30,20 @@ namespace Avalonia.Controls set { SetAndRaise(CellTemplateProperty, ref _cellTemplate, value); } } + private IDataTemplate _cellEditingCellTemplate; + + public static readonly DirectProperty CellEditingTemplateProperty = + AvaloniaProperty.RegisterDirect( + nameof(CellEditingTemplate), + o => o.CellEditingTemplate, + (o, v) => o.CellEditingTemplate = v); + + public IDataTemplate CellEditingTemplate + { + get => _cellEditingCellTemplate; + set => SetAndRaise(CellEditingTemplateProperty, ref _cellEditingCellTemplate, value); + } + private void OnCellTemplateChanged(AvaloniaPropertyChangedEventArgs e) { var oldValue = (IDataTemplate)e.OldValue; @@ -38,7 +52,7 @@ namespace Avalonia.Controls public DataGridTemplateColumn() { - IsReadOnly = true; + // IsReadOnly = true; } protected override IControl GenerateElement(DataGridCell cell, object dataItem) @@ -60,7 +74,18 @@ namespace Avalonia.Controls protected override IControl GenerateEditingElement(DataGridCell cell, object dataItem, out ICellEditBinding binding) { binding = null; - return GenerateElement(cell, dataItem); + if(CellEditingTemplate != null) + { + return CellEditingTemplate.Build(dataItem); + } + if (Design.IsDesignMode) + { + return null; + } + else + { + throw DataGridError.DataGridTemplateColumn.MissingTemplateForType(typeof(DataGridTemplateColumn)); + } } protected override object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs) @@ -70,7 +95,8 @@ namespace Avalonia.Controls protected internal override void RefreshCellContent(IControl element, string propertyName) { - if(propertyName == nameof(CellTemplate) && element.Parent is DataGridCell cell) + var cell = element.Parent as DataGridCell; + if(propertyName == nameof(CellTemplate) && cell is not null) { cell.Content = GenerateElement(cell, cell.DataContext); } From 66a02a37d09b226127b3d7636e18f0d84fbd0f02 Mon Sep 17 00:00:00 2001 From: Tim U Date: Tue, 4 Jan 2022 17:27:28 +0100 Subject: [PATCH 034/134] Handle IsReadOnly correct for DataGridTemplateColumn --- .../DataGridColumn.cs | 2 +- .../DataGridTemplateColumn.cs | 25 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 6b515503aa..8501ce3896 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -448,7 +448,7 @@ namespace Avalonia.Controls internal set; } - public bool IsReadOnly + public virtual bool IsReadOnly { get { diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs index fbdad0a8ad..e8ccb7df34 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls { public class DataGridTemplateColumn : DataGridColumn { - IDataTemplate _cellTemplate; + private IDataTemplate _cellTemplate; public static readonly DirectProperty CellTemplateProperty = AvaloniaProperty.RegisterDirect( @@ -54,7 +54,7 @@ namespace Avalonia.Controls { // IsReadOnly = true; } - + protected override IControl GenerateElement(DataGridCell cell, object dataItem) { if(CellTemplate != null) @@ -78,6 +78,10 @@ namespace Avalonia.Controls { return CellEditingTemplate.Build(dataItem); } + else if (CellTemplate != null) + { + return CellTemplate.Build(dataItem); + } if (Design.IsDesignMode) { return null; @@ -103,5 +107,22 @@ namespace Avalonia.Controls base.RefreshCellContent(element, propertyName); } + + public override bool IsReadOnly + { + get + { + if (CellEditingTemplate is null) + { + return true; + } + + return base.IsReadOnly; + } + set + { + base.IsReadOnly = value; + } + } } } From cf6c0991f8f52bcd683328dafa011550df184698 Mon Sep 17 00:00:00 2001 From: Tim U Date: Tue, 4 Jan 2022 17:27:57 +0100 Subject: [PATCH 035/134] Update Demo --- samples/ControlCatalog/Models/Person.cs | 15 +++++++++++++++ samples/ControlCatalog/Pages/DataGridPage.xaml | 12 ++++++++++++ samples/ControlCatalog/Pages/DataGridPage.xaml.cs | 6 +++--- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/Models/Person.cs b/samples/ControlCatalog/Models/Person.cs index 47f41bc584..cd70fa3959 100644 --- a/samples/ControlCatalog/Models/Person.cs +++ b/samples/ControlCatalog/Models/Person.cs @@ -16,6 +16,7 @@ namespace ControlCatalog.Models string _firstName; string _lastName; bool _isBanned; + private int _age; public string FirstName { @@ -59,6 +60,20 @@ namespace ControlCatalog.Models } } + + /// + /// Gets or sets the age of the person + /// + public int Age + { + get => _age; + set + { + _age = value; + OnPropertyChanged(nameof(Age)); + } + } + Dictionary> _errorLookup = new Dictionary>(); void SetError(string propertyName, string error) diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml b/samples/ControlCatalog/Pages/DataGridPage.xaml index 63e873d9b5..451a774cb4 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml @@ -64,6 +64,18 @@ + + + + + + + + + + + + public event EventHandler Click { - add { AddHandler(ClickEvent, value); } - remove { RemoveHandler(ClickEvent, value); } + add => AddHandler(ClickEvent, value); + remove => RemoveHandler(ClickEvent, value); } /// @@ -121,8 +127,8 @@ namespace Avalonia.Controls /// public ClickMode ClickMode { - get { return GetValue(ClickModeProperty); } - set { SetValue(ClickModeProperty, value); } + get => GetValue(ClickModeProperty); + set => SetValue(ClickModeProperty, value); } /// @@ -130,8 +136,8 @@ namespace Avalonia.Controls /// public ICommand Command { - get { return _command; } - set { SetAndRaise(CommandProperty, ref _command, value); } + get => _command; + set => SetAndRaise(CommandProperty, ref _command, value); } /// @@ -139,8 +145,8 @@ namespace Avalonia.Controls /// public KeyGesture HotKey { - get { return GetValue(HotKeyProperty); } - set { SetValue(HotKeyProperty, value); } + get => GetValue(HotKeyProperty); + set => SetValue(HotKeyProperty, value); } /// @@ -148,8 +154,8 @@ namespace Avalonia.Controls /// public object CommandParameter { - get { return GetValue(CommandParameterProperty); } - set { SetValue(CommandParameterProperty, value); } + get => GetValue(CommandParameterProperty); + set => SetValue(CommandParameterProperty, value); } /// @@ -158,8 +164,8 @@ namespace Avalonia.Controls /// public bool IsDefault { - get { return GetValue(IsDefaultProperty); } - set { SetValue(IsDefaultProperty, value); } + get => GetValue(IsDefaultProperty); + set => SetValue(IsDefaultProperty, value); } /// @@ -168,18 +174,21 @@ namespace Avalonia.Controls /// public bool IsCancel { - get { return GetValue(IsCancelProperty); } - set { SetValue(IsCancelProperty, value); } + get => GetValue(IsCancelProperty); + set => SetValue(IsCancelProperty, value); } + /// + /// Gets or sets a value indicating whether the button is currently pressed. + /// public bool IsPressed { - get { return GetValue(IsPressedProperty); } - private set { SetValue(IsPressedProperty, value); } + get => GetValue(IsPressedProperty); + private set => SetValue(IsPressedProperty, value); } /// - /// Gets or sets the Flyout that should be shown with this button + /// Gets or sets the Flyout that should be shown with this button. /// public FlyoutBase Flyout { @@ -187,7 +196,8 @@ namespace Avalonia.Controls set => SetValue(FlyoutProperty, value); } - protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + /// + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) @@ -224,6 +234,7 @@ namespace Avalonia.Controls } } + /// protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control @@ -240,6 +251,7 @@ namespace Avalonia.Controls } } + /// protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { // This will cause the hotkey manager to dispose the observer and the reference to this control @@ -358,12 +370,14 @@ namespace Avalonia.Controls } } } - + + /// protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) { IsPressed = false; } + /// protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); @@ -371,6 +385,7 @@ namespace Avalonia.Controls IsPressed = false; } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -391,6 +406,7 @@ namespace Avalonia.Controls } } + /// protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { base.UpdateDataValidation(property, value); @@ -493,7 +509,7 @@ namespace Avalonia.Controls /// /// The event sender. /// The event args. - private void CanExecuteChanged(object sender, EventArgs e) + public void CanExecuteChanged(object sender, EventArgs e) { var canExecute = Command == null || Command.CanExecute(CommandParameter); @@ -566,11 +582,12 @@ namespace Avalonia.Controls } } + /// + /// Updates the visual state of the control by applying latest PseudoClasses. + /// private void UpdatePseudoClasses(bool isPressed) { PseudoClasses.Set(":pressed", isPressed); } - - void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); } } From d8e071039bf342781ed9ed1c08d1a849588f8371 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 25 Jan 2022 20:02:40 -0500 Subject: [PATCH 107/134] Move more Button property changed handling into OnPropertyChanged override --- src/Avalonia.Controls/Button.cs | 142 ++-- .../SplitButton/SplitButton.cs | 735 ++++++++++++++++++ 2 files changed, 790 insertions(+), 87 deletions(-) create mode 100644 src/Avalonia.Controls/SplitButton/SplitButton.cs diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 770eb63266..3735e6c010 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -59,13 +59,13 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(CommandParameter)); /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty IsDefaultProperty = AvaloniaProperty.Register(nameof(IsDefault)); /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty IsCancelProperty = AvaloniaProperty.Register(nameof(IsCancel)); @@ -98,10 +98,6 @@ namespace Avalonia.Controls static Button() { FocusableProperty.OverrideDefaultValue(typeof(Button), true); - CommandProperty.Changed.Subscribe(CommandChanged); - CommandParameterProperty.Changed.Subscribe(CommandParameterChanged); - IsDefaultProperty.Changed.Subscribe(IsDefaultChanged); - IsCancelProperty.Changed.Subscribe(IsCancelChanged); AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler