From 24b2615dd0378e0688d0dc2cc5bba8828a4ee40a Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 28 Sep 2020 18:13:23 +0200 Subject: [PATCH 001/164] Fiexes issue #4657 --- src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml index 3623e95597..7dd4ed0832 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml @@ -12,6 +12,9 @@ + From 01edb8c8b128318a3c12d85be433af429dd1639f Mon Sep 17 00:00:00 2001 From: Adir Date: Wed, 7 Oct 2020 19:25:18 +0300 Subject: [PATCH 002/164] Added potential fix for hotkey manager instances not clearing up after detaching from root --- src/Avalonia.Controls/HotkeyManager.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index 95752e7875..9a2b4f2d0a 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -55,6 +55,11 @@ namespace Avalonia.Controls private void OnParentChanged(TopLevel control) { + if (control == null) + { + Stop(); + return; + } Unregister(); _root = control; Register(); From 188192c1259c34abcbcaaca1f5c4ede0d7259850 Mon Sep 17 00:00:00 2001 From: Adir Date: Wed, 7 Oct 2020 20:57:41 +0300 Subject: [PATCH 003/164] Changed HotkeyManager.cs to stop/init based on detaching/attaching to visual tree --- src/Avalonia.Controls/HotkeyManager.cs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index 9a2b4f2d0a..bff25d6dad 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -2,6 +2,7 @@ using System; using System.Windows.Input; using Avalonia.Controls.Utils; using Avalonia.Input; +using Avalonia.LogicalTree; namespace Avalonia.Controls { @@ -45,6 +46,8 @@ namespace Avalonia.Controls { _control = control; _wrapper = new HotkeyCommandWrapper(_control); + _control.DetachedFromVisualTree += ControlOnDetachedFromVisualTree; + _control.AttachedToLogicalTree += ControlOnAttachedToLogicalTree; } public void Init() @@ -53,13 +56,19 @@ namespace Avalonia.Controls _parentSub = AncestorFinder.Create(_control).Subscribe(OnParentChanged); } + private void ControlOnAttachedToLogicalTree(object sender, LogicalTreeAttachmentEventArgs e) + { + Stop(); + Init(); + } + + private void ControlOnDetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e) + { + Stop(); + } + private void OnParentChanged(TopLevel control) { - if (control == null) - { - Stop(); - return; - } Unregister(); _root = control; Register(); @@ -89,7 +98,7 @@ namespace Avalonia.Controls { if (_root != null && _hotkey != null) { - _binding = new KeyBinding() {Gesture = _hotkey, Command = _wrapper}; + _binding = new KeyBinding() { Gesture = _hotkey, Command = _wrapper }; _root.KeyBindings.Add(_binding); } } @@ -107,11 +116,12 @@ namespace Avalonia.Controls HotKeyProperty.Changed.Subscribe(args => { var control = args.Sender as IControl; - if (args.OldValue != null|| control == null) + if (args.OldValue != null || control == null) return; new Manager(control).Init(); }); } + public static void SetHotKey(AvaloniaObject target, KeyGesture value) => target.SetValue(HotKeyProperty, value); public static KeyGesture GetHotKey(AvaloniaObject target) => target.GetValue(HotKeyProperty); } From ecba5f65f05da4917b6a454e81abf3f99b76ceb4 Mon Sep 17 00:00:00 2001 From: Adir Date: Wed, 7 Oct 2020 23:11:50 +0300 Subject: [PATCH 004/164] Reverted HotkeyManager.cs Changed Button.cs to remove it's HotKey property when detaching from visual tree --- src/Avalonia.Controls/Button.cs | 12 ++++++++++-- src/Avalonia.Controls/HotkeyManager.cs | 14 -------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index e94d00b2ff..c541a384b0 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -80,6 +80,7 @@ namespace Avalonia.Controls private ICommand _command; private bool _commandCanExecute = true; + private KeyGesture _hotkey; /// /// Initializes static members of the class. @@ -168,11 +169,13 @@ namespace Avalonia.Controls private set { SetValue(IsPressedProperty, value); } } - protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { + if (_hotkey != null) + HotKey = _hotkey; base.OnAttachedToVisualTree(e); if (IsDefault) @@ -182,6 +185,7 @@ namespace Avalonia.Controls ListenForDefault(inputElement); } } + if (IsCancel) { if (e.Root is IInputElement inputElement) @@ -194,6 +198,8 @@ namespace Avalonia.Controls /// protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { + _hotkey = HotKey; + HotKey = null; base.OnDetachedFromVisualTree(e); if (IsDefault) @@ -239,6 +245,7 @@ namespace Avalonia.Controls { OnClick(); } + IsPressed = true; e.Handled = true; } @@ -255,6 +262,7 @@ namespace Avalonia.Controls { OnClick(); } + IsPressed = false; e.Handled = true; } @@ -309,7 +317,7 @@ namespace Avalonia.Controls } } } - + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) { IsPressed = false; diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index bff25d6dad..5972a0fdd9 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -2,7 +2,6 @@ using System; using System.Windows.Input; using Avalonia.Controls.Utils; using Avalonia.Input; -using Avalonia.LogicalTree; namespace Avalonia.Controls { @@ -46,8 +45,6 @@ namespace Avalonia.Controls { _control = control; _wrapper = new HotkeyCommandWrapper(_control); - _control.DetachedFromVisualTree += ControlOnDetachedFromVisualTree; - _control.AttachedToLogicalTree += ControlOnAttachedToLogicalTree; } public void Init() @@ -56,17 +53,6 @@ namespace Avalonia.Controls _parentSub = AncestorFinder.Create(_control).Subscribe(OnParentChanged); } - private void ControlOnAttachedToLogicalTree(object sender, LogicalTreeAttachmentEventArgs e) - { - Stop(); - Init(); - } - - private void ControlOnDetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e) - { - Stop(); - } - private void OnParentChanged(TopLevel control) { Unregister(); From 7c952cfc5fb3063b03b3860d7faf1b69e6db1b46 Mon Sep 17 00:00:00 2001 From: Adir Date: Thu, 8 Oct 2020 16:33:32 +0300 Subject: [PATCH 005/164] Changed Hotkey reset to detatch from logical tree (was visual tree) --- src/Avalonia.Controls/Button.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index c541a384b0..046253b3cd 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -174,8 +174,6 @@ namespace Avalonia.Controls /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { - if (_hotkey != null) - HotKey = _hotkey; base.OnAttachedToVisualTree(e); if (IsDefault) @@ -198,8 +196,6 @@ namespace Avalonia.Controls /// protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { - _hotkey = HotKey; - HotKey = null; base.OnDetachedFromVisualTree(e); if (IsDefault) @@ -213,6 +209,8 @@ namespace Avalonia.Controls protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { + if (_hotkey != null) + HotKey = _hotkey; base.OnAttachedToLogicalTree(e); if (Command != null) @@ -223,6 +221,12 @@ namespace Avalonia.Controls protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { + if (HotKey != null) + { + _hotkey = HotKey; + HotKey = null; + } + base.OnDetachedFromLogicalTree(e); if (Command != null) From 3c302f72f236a94d5b1d3f31fcab52d351d31625 Mon Sep 17 00:00:00 2001 From: Adir Date: Thu, 8 Oct 2020 18:03:52 +0300 Subject: [PATCH 006/164] Added docs explaining why need to change Hotkey on attach/detach from logical tree Added the same logic to MenuItem.cs Reverted auto indentation in Button.cs --- src/Avalonia.Controls/Button.cs | 9 ++++----- src/Avalonia.Controls/MenuItem.cs | 11 +++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 046253b3cd..87fabf5ed7 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -169,7 +169,7 @@ namespace Avalonia.Controls private set { SetValue(IsPressedProperty, value); } } - protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) @@ -183,7 +183,6 @@ namespace Avalonia.Controls ListenForDefault(inputElement); } } - if (IsCancel) { if (e.Root is IInputElement inputElement) @@ -210,6 +209,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 HotKey = _hotkey; base.OnAttachedToLogicalTree(e); @@ -223,6 +223,7 @@ namespace Avalonia.Controls { if (HotKey != null) { + // This will cause the hotkey manager to dispose the observer and the reference to this control _hotkey = HotKey; HotKey = null; } @@ -249,7 +250,6 @@ namespace Avalonia.Controls { OnClick(); } - IsPressed = true; e.Handled = true; } @@ -266,7 +266,6 @@ namespace Avalonia.Controls { OnClick(); } - IsPressed = false; e.Handled = true; } @@ -321,7 +320,7 @@ namespace Avalonia.Controls } } } - + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) { IsPressed = false; diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 7d4fef009d..faf92c97fe 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -102,6 +102,7 @@ namespace Avalonia.Controls private ICommand? _command; private bool _commandCanExecute = true; private Popup? _popup; + private KeyGesture _hotkey; /// /// Initializes static members of the class. @@ -337,6 +338,9 @@ 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 + HotKey = _hotkey; base.OnAttachedToLogicalTree(e); if (Command != null) @@ -347,6 +351,13 @@ namespace Avalonia.Controls protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { + if (HotKey != null) + { + // This will cause the hotkey manager to dispose the observer and the reference to this control + _hotkey = HotKey; + HotKey = null; + } + base.OnDetachedFromLogicalTree(e); if (Command != null) From 68f67f703401c50105ed63250266f44ec7f190a7 Mon Sep 17 00:00:00 2001 From: Adir Date: Thu, 8 Oct 2020 18:05:46 +0300 Subject: [PATCH 007/164] Reverted auto indentation in HotkeyManager.cs --- src/Avalonia.Controls/HotkeyManager.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index 5972a0fdd9..95752e7875 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -84,7 +84,7 @@ namespace Avalonia.Controls { if (_root != null && _hotkey != null) { - _binding = new KeyBinding() { Gesture = _hotkey, Command = _wrapper }; + _binding = new KeyBinding() {Gesture = _hotkey, Command = _wrapper}; _root.KeyBindings.Add(_binding); } } @@ -102,12 +102,11 @@ namespace Avalonia.Controls HotKeyProperty.Changed.Subscribe(args => { var control = args.Sender as IControl; - if (args.OldValue != null || control == null) + if (args.OldValue != null|| control == null) return; new Manager(control).Init(); }); } - public static void SetHotKey(AvaloniaObject target, KeyGesture value) => target.SetValue(HotKeyProperty, value); public static KeyGesture GetHotKey(AvaloniaObject target) => target.GetValue(HotKeyProperty); } From fd2da7aa49b2ba59289d205e35b39cff177b43db Mon Sep 17 00:00:00 2001 From: Adir Date: Thu, 8 Oct 2020 21:26:21 +0300 Subject: [PATCH 008/164] Added unit tests for enforcing manager releasing reference to the controls Changed AncestorFinder.cs to dispose child nodes and subject (need review on this) --- src/Avalonia.Controls/Utils/AncestorFinder.cs | 2 + .../Utils/HotKeyManagerTests.cs | 113 +++++++++++++++++- 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Utils/AncestorFinder.cs b/src/Avalonia.Controls/Utils/AncestorFinder.cs index 529aec1393..42e0d85dcd 100644 --- a/src/Avalonia.Controls/Utils/AncestorFinder.cs +++ b/src/Avalonia.Controls/Utils/AncestorFinder.cs @@ -47,6 +47,8 @@ namespace Avalonia.Controls.Utils public void Dispose() { + _child?.Dispose(); + _subject.Dispose(); _disposable.Dispose(); } } diff --git a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs index 9f2712c93c..6425c5c8f4 100644 --- a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs @@ -1,9 +1,14 @@ -using Moq; +using System; +using System.Collections.Generic; +using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.Platform; using Avalonia.Styling; +using Avalonia.UnitTests; +using Moq; using Xunit; namespace Avalonia.Controls.UnitTests.Utils @@ -50,10 +55,116 @@ namespace Avalonia.Controls.UnitTests.Utils HotKeyManager.SetHotKey(button, null); Assert.Empty(tl.KeyBindings); + } + } + + [Fact] + public void HotKeyManager_Release_Reference_When_Control_Detached() + { + using (AvaloniaLocator.EnterScope()) + { + var styler = new Mock(); + + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(new WindowingPlatformMock()) + .Bind().ToConstant(styler.Object); + + var gesture1 = new KeyGesture(Key.A, KeyModifiers.Control); + + WeakReference reference = null; + + var tl = new Window(); + + new Action(() => + { + var button = new Button(); + reference = new WeakReference(button, true); + tl.Content = button; + tl.Template = CreateWindowTemplate(); + tl.ApplyTemplate(); + tl.Presenter.ApplyTemplate(); + HotKeyManager.SetHotKey(button, gesture1); + + // Detach the button from the logical tree, so there is no reference to it + tl.Content = null; + tl.ApplyTemplate(); + })(); + + // The button should be collected since it's detached from the listbox + GC.Collect(); + GC.WaitForPendingFinalizers(); + + Assert.Null(reference?.Target); + } + } + + [Fact] + public void HotKeyManager_Release_Reference_When_Control_In_Item_Template_Detached() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var styler = new Mock(); + + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(new WindowingPlatformMock()) + .Bind().ToConstant(styler.Object); + + var gesture1 = new KeyGesture(Key.A, KeyModifiers.Control); + + var weakReferences = new List(); + var tl = new Window { SizeToContent = SizeToContent.WidthAndHeight, IsVisible = true }; + var lm = tl.LayoutManager; + + var keyGestures = new AvaloniaList { gesture1 }; + var listBox = new ListBox + { + Width = 100, + Height = 100, + VirtualizationMode = ItemVirtualizationMode.None, + // Create a button with binding to the KeyGesture in the template and add it to references list + ItemTemplate = new FuncDataTemplate(typeof(KeyGesture), (o, scope) => + { + var keyGesture = o as KeyGesture; + var button = new Button + { + DataContext = keyGesture, [!Button.HotKeyProperty] = new Binding("") + }; + weakReferences.Add(new WeakReference(button, true)); + return button; + }) + }; + // Add the listbox and render it + tl.Content = listBox; + lm.ExecuteInitialLayoutPass(); + listBox.Items = keyGestures; + lm.ExecuteLayoutPass(); + + // Let the button detach when clearing the source items + keyGestures.Clear(); + lm.ExecuteLayoutPass(); + + // Add it again to double check,and render + keyGestures.Add(gesture1); + lm.ExecuteLayoutPass(); + + keyGestures.Clear(); + lm.ExecuteLayoutPass(); + + // The button should be collected since it's detached from the listbox + GC.Collect(); + GC.WaitForPendingFinalizers(); + + + Assert.True(weakReferences.Count > 0); + foreach (var weakReference in weakReferences) + { + Assert.Null(weakReference.Target); + } } } + private FuncControlTemplate CreateWindowTemplate() { return new FuncControlTemplate((parent, scope) => From d58d80d11b8c8e7d038899e99c62e75da59fbc2c Mon Sep 17 00:00:00 2001 From: Adir Date: Fri, 9 Oct 2020 01:34:14 +0300 Subject: [PATCH 009/164] Added another GC collect to the HotKeyManagerTests.cs memory leak tests Renamed tests --- .../Utils/HotKeyManagerTests.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs index 6425c5c8f4..2e23de366e 100644 --- a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs @@ -59,7 +59,7 @@ namespace Avalonia.Controls.UnitTests.Utils } [Fact] - public void HotKeyManager_Release_Reference_When_Control_Detached() + public void HotKeyManager_Should_Release_Reference_When_Control_Detached() { using (AvaloniaLocator.EnterScope()) { @@ -94,13 +94,15 @@ namespace Avalonia.Controls.UnitTests.Utils // The button should be collected since it's detached from the listbox GC.Collect(); GC.WaitForPendingFinalizers(); + GC.Collect(); + GC.WaitForPendingFinalizers(); Assert.Null(reference?.Target); } } [Fact] - public void HotKeyManager_Release_Reference_When_Control_In_Item_Template_Detached() + public void HotKeyManager_Should_Release_Reference_When_Control_In_Item_Template_Detached() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -154,7 +156,8 @@ namespace Avalonia.Controls.UnitTests.Utils // The button should be collected since it's detached from the listbox GC.Collect(); GC.WaitForPendingFinalizers(); - + GC.Collect(); + GC.WaitForPendingFinalizers(); Assert.True(weakReferences.Count > 0); foreach (var weakReference in weakReferences) From 640bddf5cabf5f6a8bf4cb07fefe341549c62c03 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 24 Sep 2020 14:59:29 +0200 Subject: [PATCH 010/164] Add interface ICommandSource --- src/Avalonia.Input/ICommandSource.cs | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/Avalonia.Input/ICommandSource.cs diff --git a/src/Avalonia.Input/ICommandSource.cs b/src/Avalonia.Input/ICommandSource.cs new file mode 100644 index 0000000000..b0dd75e12e --- /dev/null +++ b/src/Avalonia.Input/ICommandSource.cs @@ -0,0 +1,31 @@ +using System.Windows.Input; + +namespace Avalonia.Input +{ + /// + /// An interface for classes that know how to invoke a Command. + /// + public interface ICommandSource + { + /// + /// The command that will be executed when the class is "invoked." + /// Classes that implement this interface should enable or disable based on the command's CanExecute return value. + /// The property may be implemented as read-write if desired. + /// + ICommand Command { get; } + + /// + /// The parameter that will be passed to the command when executing the command. + /// The property may be implemented as read-write if desired. + /// + object CommandParameter { get; } + + + /// + /// Bor the bheavior CanExecuteChanged + /// + /// + /// + void CanExecuteChanged(object sender, System.EventArgs e); + } +} From ef385675470f347aee079f3c9517f88014aa3789 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 24 Sep 2020 15:48:10 +0200 Subject: [PATCH 011/164] Implemented ICommandSource --- src/Avalonia.Controls/Button.cs | 4 +++- src/Avalonia.Controls/MenuItem.cs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index e94d00b2ff..91eef3947b 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -30,7 +30,7 @@ namespace Avalonia.Controls /// A button control. /// [PseudoClasses(":pressed")] - public class Button : ContentControl + public class Button : ContentControl, ICommandSource { /// /// Defines the property. @@ -492,5 +492,7 @@ namespace Avalonia.Controls { PseudoClasses.Set(":pressed", isPressed); } + + void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); } } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 7d4fef009d..d034f54df2 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -22,7 +22,7 @@ namespace Avalonia.Controls /// A menu item control. /// [PseudoClasses(":separator", ":icon", ":open", ":pressed", ":selected")] - public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable + public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable, ICommandSource { /// /// Defines the property. @@ -609,6 +609,8 @@ namespace Avalonia.Controls SelectedItem = null; } + void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); + /// /// A dependency resolver which returns a . /// From fa1476a6622101c383a19e499eb44ad352306e14 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 24 Sep 2020 16:05:59 +0200 Subject: [PATCH 012/164] Fix Hotkey CommandParameter --- src/Avalonia.Controls/HotkeyManager.cs | 17 +++++++++++------ src/Avalonia.Controls/NativeMenuItem.cs | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index 95752e7875..b104b7a9a8 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -1,6 +1,7 @@ using System; using System.Windows.Input; using Avalonia.Controls.Utils; +using Avalonia.Data.Core; using Avalonia.Input; namespace Avalonia.Controls @@ -12,14 +13,14 @@ namespace Avalonia.Controls class HotkeyCommandWrapper : ICommand { - public HotkeyCommandWrapper(IControl control) + public HotkeyCommandWrapper(ICommandSource control) { - Control = control; + CommandSource = control; } - public readonly IControl Control; + public readonly ICommandSource CommandSource; - private ICommand GetCommand() => Control.GetValue(Button.CommandProperty); + private ICommand GetCommand() => CommandSource.Command; public bool CanExecute(object parameter) => GetCommand()?.CanExecute(parameter) ?? false; @@ -37,6 +38,7 @@ namespace Avalonia.Controls private TopLevel _root; private IDisposable _parentSub; private IDisposable _hotkeySub; + private IDisposable _commandParameterChangedSubscriber; private KeyGesture _hotkey; private readonly HotkeyCommandWrapper _wrapper; private KeyBinding _binding; @@ -44,7 +46,7 @@ namespace Avalonia.Controls public Manager(IControl control) { _control = control; - _wrapper = new HotkeyCommandWrapper(_control); + _wrapper = new HotkeyCommandWrapper(_control as ICommandSource); } public void Init() @@ -77,6 +79,7 @@ namespace Avalonia.Controls { if (_root != null && _binding != null) _root.KeyBindings.Remove(_binding); + _commandParameterChangedSubscriber?.Dispose(); _binding = null; } @@ -85,6 +88,8 @@ namespace Avalonia.Controls if (_root != null && _hotkey != null) { _binding = new KeyBinding() {Gesture = _hotkey, Command = _wrapper}; + _commandParameterChangedSubscriber = _binding.Bind(KeyBinding.CommandParameterProperty + , ExpressionObserver.Create(((ICommandSource)_control), o => o.CommandParameter)); _root.KeyBindings.Add(_binding); } } @@ -102,7 +107,7 @@ namespace Avalonia.Controls HotKeyProperty.Changed.Subscribe(args => { var control = args.Sender as IControl; - if (args.OldValue != null|| control == null) + if (args.OldValue != null || control == null || !(control is ICommandSource)) return; new Manager(control).Init(); }); diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index a0fec9e677..d1adda3b0d 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -148,7 +148,7 @@ namespace Avalonia.Controls void CanExecuteChanged() { - IsEnabled = _command?.CanExecute(null) ?? true; + IsEnabled = _command?.CanExecute(CommandParameter) ?? true; } public bool HasClickHandlers => Clicked != null; From 9ffd78a3baa31772de40dd3aad018451486927e7 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 24 Sep 2020 16:29:51 +0200 Subject: [PATCH 013/164] Add Test --- .../Utils/HotKeyManagerTests.cs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs index 9f2712c93c..a29eb0f0c7 100644 --- a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs @@ -5,6 +5,9 @@ using Avalonia.Input; using Avalonia.Platform; using Avalonia.Styling; using Xunit; +using System; +using Avalonia.Input.Raw; +using Factory = System.Func, Avalonia.Controls.Window, Avalonia.AvaloniaObject>; namespace Avalonia.Controls.UnitTests.Utils { @@ -54,6 +57,84 @@ namespace Avalonia.Controls.UnitTests.Utils } } + [Theory] + [MemberData(nameof(ElementsFactory))] + public void HotKeyManager_Should_Use_CommandParameter(string factoryName, Factory factory) + { + using (AvaloniaLocator.EnterScope()) + { + var styler = new Mock(); + var target = new KeyboardDevice(); + var commandResult = 0; + var expectedParameter = 1; + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(new WindowingPlatformMock()) + .Bind().ToConstant(styler.Object); + + var gesture = new KeyGesture(Key.A, KeyModifiers.Control); + + var action = new Action(parameter => + { + if (parameter is int value) + { + commandResult = value; + } + }); + + var root = new Window(); + var element = factory(expectedParameter, action, root); + + root.Template = CreateWindowTemplate(); + root.ApplyTemplate(); + root.Presenter.ApplyTemplate(); + + HotKeyManager.SetHotKey(element, gesture); + + target.ProcessRawEvent(new RawKeyEventArgs(target, + 0, + root, + RawKeyEventType.KeyDown, + Key.A, + RawInputModifiers.Control)); + + Assert.True(expectedParameter == commandResult, $"{factoryName} HotKey did not carry the CommandParameter."); + } + } + + public static TheoryData ElementsFactory => + new TheoryData() + { + {nameof(Button), MakeButton}, + {nameof(MenuItem),MakeMenu}, + }; + + private static AvaloniaObject MakeMenu(int expectedParameter, Action action, Window root) + { + var menuitem = new MenuItem() + { + Command = new Command(action), + CommandParameter = expectedParameter, + }; + var rootMenu = new Menu(); + + rootMenu.Items = new[] { menuitem }; + + root.Content = rootMenu; + return menuitem; + } + + private static AvaloniaObject MakeButton(int expectedParameter, Action action, Window root) + { + var button = new Button() + { + Command = new Command(action), + CommandParameter = expectedParameter, + }; + + root.Content = button; + return button; + } + private FuncControlTemplate CreateWindowTemplate() { return new FuncControlTemplate((parent, scope) => @@ -65,5 +146,22 @@ namespace Avalonia.Controls.UnitTests.Utils }.RegisterInNameScope(scope); }); } + + class Command : System.Windows.Input.ICommand + { + private readonly Action _execeute; + +#pragma warning disable 67 // Event not used + public event EventHandler CanExecuteChanged; +#pragma warning restore 67 // Event not used + + public Command(Action execeute) + { + _execeute = execeute; + } + public bool CanExecute(object parameter) => true; + + public void Execute(object parameter) => _execeute?.Invoke(parameter); + } } } From f1f2df8dd48d42ddc7cc669235988b38cf24b259 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 25 Sep 2020 17:32:26 +0200 Subject: [PATCH 014/164] Fixes Issue #4734 : HotKeys trigger command when control is disabled --- src/Avalonia.Controls/HotkeyManager.cs | 2 +- src/Avalonia.Input/ICommandSource.cs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index b104b7a9a8..f210ecfdaf 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -22,7 +22,7 @@ namespace Avalonia.Controls private ICommand GetCommand() => CommandSource.Command; - public bool CanExecute(object parameter) => GetCommand()?.CanExecute(parameter) ?? false; + public bool CanExecute(object parameter) => CommandSource.IsEffectivelyEnabled; public void Execute(object parameter) => GetCommand()?.Execute(parameter); diff --git a/src/Avalonia.Input/ICommandSource.cs b/src/Avalonia.Input/ICommandSource.cs index b0dd75e12e..ba2e8eed4e 100644 --- a/src/Avalonia.Input/ICommandSource.cs +++ b/src/Avalonia.Input/ICommandSource.cs @@ -27,5 +27,10 @@ namespace Avalonia.Input /// /// void CanExecuteChanged(object sender, System.EventArgs e); + + /// + /// Gets a value indicating whether this control and all its parents are enabled. + /// + bool IsEffectivelyEnabled { get; } } } From f0efe9aef0d2e4953f5ed034e21359c95dcc4499 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 25 Sep 2020 17:33:13 +0200 Subject: [PATCH 015/164] Add test Issue #4734 --- .../Utils/HotKeyManagerTests.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs index a29eb0f0c7..151c3070f4 100644 --- a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs @@ -101,6 +101,49 @@ namespace Avalonia.Controls.UnitTests.Utils } } + + [Theory] + [MemberData(nameof(ElementsFactory))] + public void HotKeyManager_Should_Do_Not_Executed_When_IsEnabled_False(string factoryName, Factory factory) + { + using (AvaloniaLocator.EnterScope()) + { + var styler = new Mock(); + var target = new KeyboardDevice(); + var isExecuted = false; + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(new WindowingPlatformMock()) + .Bind().ToConstant(styler.Object); + + var gesture = new KeyGesture(Key.A, KeyModifiers.Control); + + var action = new Action(parameter => + { + isExecuted = true; + }); + + var root = new Window(); + var element = factory(0, action, root) as InputElement; + + element.IsEnabled = false; + + root.Template = CreateWindowTemplate(); + root.ApplyTemplate(); + root.Presenter.ApplyTemplate(); + + HotKeyManager.SetHotKey(element, gesture); + + target.ProcessRawEvent(new RawKeyEventArgs(target, + 0, + root, + RawKeyEventType.KeyDown, + Key.A, + RawInputModifiers.Control)); + + Assert.True(isExecuted == false, $"{factoryName} Execution raised when IsEnabled is false."); + } + } + public static TheoryData ElementsFactory => new TheoryData() { From c7a21d7268aad472e9022777b626120cdc6b29e3 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 9 Oct 2020 09:51:54 +0200 Subject: [PATCH 016/164] Removed CommandParameter Binding --- src/Avalonia.Controls/HotkeyManager.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index f210ecfdaf..dabc35dd24 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -1,7 +1,6 @@ using System; using System.Windows.Input; using Avalonia.Controls.Utils; -using Avalonia.Data.Core; using Avalonia.Input; namespace Avalonia.Controls @@ -22,9 +21,12 @@ namespace Avalonia.Controls private ICommand GetCommand() => CommandSource.Command; - public bool CanExecute(object parameter) => CommandSource.IsEffectivelyEnabled; + public bool CanExecute(object parameter) => + CommandSource.Command?.CanExecute(CommandSource.CommandParameter) == true + && CommandSource.IsEffectivelyEnabled; - public void Execute(object parameter) => GetCommand()?.Execute(parameter); + public void Execute(object parameter) => + GetCommand()?.Execute(CommandSource.CommandParameter); #pragma warning disable 67 // Event not used public event EventHandler CanExecuteChanged; @@ -38,7 +40,6 @@ namespace Avalonia.Controls private TopLevel _root; private IDisposable _parentSub; private IDisposable _hotkeySub; - private IDisposable _commandParameterChangedSubscriber; private KeyGesture _hotkey; private readonly HotkeyCommandWrapper _wrapper; private KeyBinding _binding; @@ -79,7 +80,6 @@ namespace Avalonia.Controls { if (_root != null && _binding != null) _root.KeyBindings.Remove(_binding); - _commandParameterChangedSubscriber?.Dispose(); _binding = null; } @@ -87,9 +87,7 @@ namespace Avalonia.Controls { if (_root != null && _hotkey != null) { - _binding = new KeyBinding() {Gesture = _hotkey, Command = _wrapper}; - _commandParameterChangedSubscriber = _binding.Bind(KeyBinding.CommandParameterProperty - , ExpressionObserver.Create(((ICommandSource)_control), o => o.CommandParameter)); + _binding = new KeyBinding() { Gesture = _hotkey, Command = _wrapper }; _root.KeyBindings.Add(_binding); } } From b8728c8f8ccf08a95f68114d0c3f2f00bb7ff0d3 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 9 Oct 2020 09:53:54 +0200 Subject: [PATCH 017/164] Add warning when HotKey is attached to a control that does not support ICommandSource. --- src/Avalonia.Controls/HotkeyManager.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index dabc35dd24..baae37e022 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -105,8 +105,13 @@ namespace Avalonia.Controls HotKeyProperty.Changed.Subscribe(args => { var control = args.Sender as IControl; - if (args.OldValue != null || control == null || !(control is ICommandSource)) + if (args.OldValue != null || control == null || !(control is ICommandSource)) + { + Logging.Logger.TryGet(Logging.LogEventLevel.Warning, Logging.LogArea.Control)?. + Log(control, $"The element {args.Sender.GetType().Name} do not support HotKey({args.NewValue})."); return; + } + new Manager(control).Init(); }); } From abff848c8d844eebd32a8a61062a471ccc807db7 Mon Sep 17 00:00:00 2001 From: workgroupengineering Date: Tue, 13 Oct 2020 17:01:37 +0200 Subject: [PATCH 018/164] Update warning message Co-authored-by: Steven Kirk --- src/Avalonia.Controls/HotkeyManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index baae37e022..b76b7fbed5 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -108,7 +108,7 @@ namespace Avalonia.Controls if (args.OldValue != null || control == null || !(control is ICommandSource)) { Logging.Logger.TryGet(Logging.LogEventLevel.Warning, Logging.LogArea.Control)?. - Log(control, $"The element {args.Sender.GetType().Name} do not support HotKey({args.NewValue})."); + Log(control, $"The element {args.Sender.GetType().Name} does not support binding a HotKey ({args.NewValue})."); return; } From fda517d13e7371c4577974dbc4ea3d6bc5d78201 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 17 Oct 2020 14:24:52 +0200 Subject: [PATCH 019/164] Make filename match contained class. --- src/Avalonia.Input/{Cursors.cs => Cursor.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Avalonia.Input/{Cursors.cs => Cursor.cs} (100%) diff --git a/src/Avalonia.Input/Cursors.cs b/src/Avalonia.Input/Cursor.cs similarity index 100% rename from src/Avalonia.Input/Cursors.cs rename to src/Avalonia.Input/Cursor.cs From 3124152913cf91658836e7d9b4faef246a573178 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 17 Oct 2020 16:37:27 +0200 Subject: [PATCH 020/164] Enable NRT for cursor API. --- src/Avalonia.Input/Cursor.cs | 2 ++ src/Avalonia.Input/Platform/IStandardCursorFactory.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Avalonia.Input/Cursor.cs b/src/Avalonia.Input/Cursor.cs index 920b598eac..0c858aacdd 100644 --- a/src/Avalonia.Input/Cursor.cs +++ b/src/Avalonia.Input/Cursor.cs @@ -1,6 +1,8 @@ using System; using Avalonia.Platform; +#nullable enable + namespace Avalonia.Input { /* diff --git a/src/Avalonia.Input/Platform/IStandardCursorFactory.cs b/src/Avalonia.Input/Platform/IStandardCursorFactory.cs index 51845cf03e..ede19e91b4 100644 --- a/src/Avalonia.Input/Platform/IStandardCursorFactory.cs +++ b/src/Avalonia.Input/Platform/IStandardCursorFactory.cs @@ -1,5 +1,7 @@ using Avalonia.Input; +#nullable enable + namespace Avalonia.Platform { public interface IStandardCursorFactory From 03a18f56bbc6c70542db99e0056d94b360a78d91 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 17 Oct 2020 16:38:24 +0200 Subject: [PATCH 021/164] Initial support for custom cursors on Win32. --- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 1 + .../HeadlessPlatformStubs.cs | 6 +- src/Avalonia.Input/Cursor.cs | 20 ++--- .../Platform/IStandardCursorFactory.cs | 1 + src/Avalonia.Native/Cursor.cs | 5 ++ src/Avalonia.X11/X11CursorFactory.cs | 5 ++ src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs | 5 ++ src/Windows/Avalonia.Win32/CursorFactory.cs | 80 +++++++++++++++++++ .../Interop/UnmanagedMethods.cs | 13 +++ .../CursorFactoryMock.cs | 5 ++ 10 files changed, 130 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index f377b1bcd1..a400102877 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -195,6 +195,7 @@ namespace Avalonia.DesignerSupport.Remote class CursorFactoryStub : IStandardCursorFactory { public IPlatformHandle GetCursor(StandardCursorType cursorType) => new PlatformHandle(IntPtr.Zero, "STUB"); + public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new PlatformHandle(IntPtr.Zero, "STUB"); } class IconLoaderStub : IPlatformIconLoader diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index 4c0e2982f4..43e6731eee 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -54,11 +54,15 @@ namespace Avalonia.Headless class HeadlessCursorFactoryStub : IStandardCursorFactory { - public IPlatformHandle GetCursor(StandardCursorType cursorType) { return new PlatformHandle(new IntPtr((int)cursorType), "STUB"); } + + public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + return new PlatformHandle(IntPtr.Zero, "STUB"); + } } class HeadlessPlatformSettingsStub : IPlatformSettings diff --git a/src/Avalonia.Input/Cursor.cs b/src/Avalonia.Input/Cursor.cs index 0c858aacdd..21c2b1a326 100644 --- a/src/Avalonia.Input/Cursor.cs +++ b/src/Avalonia.Input/Cursor.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Media.Imaging; using Avalonia.Platform; #nullable enable @@ -58,7 +59,12 @@ namespace Avalonia.Input } public Cursor(StandardCursorType cursorType) - : this(GetCursor(cursorType)) + : this(GetCursorFactory().GetCursor(cursorType)) + { + } + + public Cursor(IBitmap cursor, PixelPoint hotSpot) + : this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot)) { } @@ -71,16 +77,10 @@ namespace Avalonia.Input throw new ArgumentException($"Unrecognized cursor type '{s}'."); } - private static IPlatformHandle GetCursor(StandardCursorType type) + private static IStandardCursorFactory GetCursorFactory() { - var platform = AvaloniaLocator.Current.GetService(); - - if (platform == null) - { - throw new Exception("Could not create Cursor: IStandardCursorFactory not registered."); - } - - return platform.GetCursor(type); + return AvaloniaLocator.Current.GetService() ?? + throw new Exception("Could not create Cursor: ICursorFactory not registered."); } } } diff --git a/src/Avalonia.Input/Platform/IStandardCursorFactory.cs b/src/Avalonia.Input/Platform/IStandardCursorFactory.cs index ede19e91b4..12c3ec7b74 100644 --- a/src/Avalonia.Input/Platform/IStandardCursorFactory.cs +++ b/src/Avalonia.Input/Platform/IStandardCursorFactory.cs @@ -7,5 +7,6 @@ namespace Avalonia.Platform public interface IStandardCursorFactory { IPlatformHandle GetCursor(StandardCursorType cursorType); + IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot); } } diff --git a/src/Avalonia.Native/Cursor.cs b/src/Avalonia.Native/Cursor.cs index 3c65367c12..fffff51bce 100644 --- a/src/Avalonia.Native/Cursor.cs +++ b/src/Avalonia.Native/Cursor.cs @@ -38,5 +38,10 @@ namespace Avalonia.Native var cursor = _native.GetCursor((AvnStandardCursorType)cursorType); return new AvaloniaNativeCursor( cursor ); } + + public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + throw new NotImplementedException(); + } } } diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 1c995475ae..a82fe76cd5 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -67,6 +67,11 @@ namespace Avalonia.X11 return new PlatformHandle(handle, "XCURSOR"); } + public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + throw new NotImplementedException(); + } + private static IntPtr GetNullCursor(IntPtr display) { XColor color = new XColor(); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs index 7a257da0dd..8763f18cc0 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs @@ -10,6 +10,11 @@ namespace Avalonia.LinuxFramebuffer { return new PlatformHandle(IntPtr.Zero, null); } + + public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + return new PlatformHandle(IntPtr.Zero, null); + } } internal class PlatformSettings : IPlatformSettings { diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index 8c40eaa7b8..844f230a2e 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -1,8 +1,15 @@ using System; using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Runtime.InteropServices; using Avalonia.Input; +using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Win32.Interop; +using SdBitmap = System.Drawing.Bitmap; +using SdPixelFormat = System.Drawing.Imaging.PixelFormat; namespace Avalonia.Win32 { @@ -87,5 +94,78 @@ namespace Avalonia.Win32 return rv; } + + public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + using var source = LoadSystemDrawingBitmap(cursor); + using var mask = AlphaToMask(source); + + var info = new UnmanagedMethods.ICONINFO + { + IsIcon = false, + xHotspot = hotSpot.X, + yHotspot = hotSpot.Y, + MaskBitmap = mask.GetHbitmap(), + ColorBitmap = source.GetHbitmap(), + }; + + return new PlatformHandle( + UnmanagedMethods.CreateIconIndirect(ref info), + PlatformConstants.CursorHandleType); + } + + private SdBitmap LoadSystemDrawingBitmap(IBitmapImpl bitmap) + { + using var memoryStream = new MemoryStream(); + bitmap.Save(memoryStream); + return new SdBitmap(memoryStream); + } + + private unsafe SdBitmap AlphaToMask(SdBitmap source) + { + var dest = new SdBitmap(source.Width, source.Height, SdPixelFormat.Format1bppIndexed); + + if (source.PixelFormat != SdPixelFormat.Format32bppArgb && + source.PixelFormat != SdPixelFormat.Format32bppPArgb) + { + return dest; + } + + var sourceData = source.LockBits( + new Rectangle(default, source.Size), + ImageLockMode.ReadOnly, + SdPixelFormat.Format32bppArgb); + var destData = dest.LockBits( + new Rectangle(default, source.Size), + ImageLockMode.ReadOnly, + SdPixelFormat.Format1bppIndexed); + + try + { + var pSource = (byte*)sourceData.Scan0.ToPointer(); + var pDest = (byte*)destData.Scan0.ToPointer(); + + for (var y = 0; y < dest.Height; ++y) + { + for (var x = 0; x < dest.Width; ++x) + { + if (pSource[x * 4] == 0) + { + pDest[x / 8] |= (byte)(1 << (x % 8)); + } + } + + pSource += sourceData.Stride; + pDest += destData.Stride; + } + + return dest; + } + finally + { + source.UnlockBits(sourceData); + dest.UnlockBits(destData); + } + } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 5feb6c9e46..c18a5c07c5 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1031,6 +1031,9 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern IntPtr LoadCursor(IntPtr hInstance, IntPtr lpCursorName); + [DllImport("user32.dll")] + public static extern IntPtr CreateIconIndirect([In] ref ICONINFO iconInfo); + [DllImport("user32.dll")] public static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg); @@ -1743,6 +1746,16 @@ namespace Avalonia.Win32.Interop public int CyContact; } + [StructLayout(LayoutKind.Sequential)] + public struct ICONINFO + { + public bool IsIcon; + public int xHotspot; + public int yHotspot; + public IntPtr MaskBitmap; + public IntPtr ColorBitmap; + }; + [Flags] public enum TouchInputFlags { diff --git a/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs b/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs index da02cccdc5..66c6033fb4 100644 --- a/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs +++ b/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs @@ -10,5 +10,10 @@ namespace Avalonia.Controls.UnitTests { return new PlatformHandle(IntPtr.Zero, cursorType.ToString()); } + + public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + return new PlatformHandle(IntPtr.Zero, "Custom"); + } } } From 193cdb3324d82a3dc7207a5bf980fa45bd9b639e Mon Sep 17 00:00:00 2001 From: grokys Date: Mon, 19 Oct 2020 13:44:54 +0200 Subject: [PATCH 022/164] Implement custom cursors on X11. --- src/Avalonia.X11/X11CursorFactory.cs | 59 +++++++++++++++++++++++++++- src/Avalonia.X11/X11Structs.cs | 2 +- src/Avalonia.X11/XLib.cs | 7 ++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index a82fe76cd5..176878ed5f 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -1,8 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; +using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; using Avalonia.Platform; +using Avalonia.Utilities; + +#nullable enable namespace Avalonia.X11 { @@ -67,9 +72,9 @@ namespace Avalonia.X11 return new PlatformHandle(handle, "XCURSOR"); } - public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + public unsafe IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) { - throw new NotImplementedException(); + return new XImageCursor(_display, cursor, hotSpot); } private static IntPtr GetNullCursor(IntPtr display) @@ -79,5 +84,55 @@ namespace Avalonia.X11 IntPtr pixmap = XLib.XCreateBitmapFromData(display, window, NullCursorData, 1, 1); return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); } + + private unsafe class XImageCursor : IFramebufferPlatformSurface, IPlatformHandle, IDisposable + { + private readonly PixelSize _pixelSize; + private readonly IUnmanagedBlob _blob; + + public XImageCursor(IntPtr display, IBitmapImpl bitmap, PixelPoint hotSpot) + { + var size = Marshal.SizeOf() + + (bitmap.PixelSize.Width * bitmap.PixelSize.Height * 4); + + _pixelSize = bitmap.PixelSize; + _blob = AvaloniaLocator.Current.GetService().AllocBlob(size); + + var image = (XcursorImage*)_blob.Address; + image->version = 1; + image->size = Marshal.SizeOf(); + image->width = bitmap.PixelSize.Width; + image->height = bitmap.PixelSize.Height; + image->xhot = hotSpot.X; + image->yhot = hotSpot.Y; + image->pixels = (IntPtr)(image + 1); + + using (var renderTarget = AvaloniaLocator.Current.GetService().CreateRenderTarget(new[] { this })) + using (var ctx = renderTarget.CreateDrawingContext(null)) + { + var r = new Rect(_pixelSize.ToSize(1)); + ctx.DrawBitmap(RefCountable.CreateUnownedNotClonable(bitmap), 1, r, r); + } + + Handle = XLib.XcursorImageLoadCursor(display, _blob.Address); + } + + public IntPtr Handle { get; } + public string HandleDescriptor => "XCURSOR"; + + public void Dispose() + { + XLib.XcursorImageDestroy(Handle); + _blob.Dispose(); + } + + public ILockedFramebuffer Lock() + { + return new LockedFramebuffer( + _blob.Address + Marshal.SizeOf(), + _pixelSize, _pixelSize.Width * 4, + new Vector(96, 96), PixelFormat.Bgra8888, null); + } + } } } diff --git a/src/Avalonia.X11/X11Structs.cs b/src/Avalonia.X11/X11Structs.cs index 0ae5c16aef..7ee764e8a3 100644 --- a/src/Avalonia.X11/X11Structs.cs +++ b/src/Avalonia.X11/X11Structs.cs @@ -1684,7 +1684,7 @@ namespace Avalonia.X11 { [StructLayout (LayoutKind.Sequential)] internal struct XcursorImage { - private int version; + public int version; public int size; /* nominal size for matching */ public int width; /* actual width */ public int height; /* actual height */ diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index 85aa4862b7..f5da691caa 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/src/Avalonia.X11/XLib.cs @@ -19,6 +19,7 @@ namespace Avalonia.X11 const string libX11Randr = "libXrandr.so.2"; const string libX11Ext = "libXext.so.6"; const string libXInput = "libXi.so.6"; + const string libXCursor = "libXcursor.so.1"; [DllImport(libX11)] public static extern IntPtr XOpenDisplay(IntPtr display); @@ -512,6 +513,12 @@ namespace Avalonia.X11 [DllImport(libXInput)] public static extern void XIFreeDeviceInfo(XIDeviceInfo* info); + [DllImport(libXCursor)] + public static extern IntPtr XcursorImageLoadCursor(IntPtr display, IntPtr image); + + [DllImport(libXCursor)] + public static extern IntPtr XcursorImageDestroy(IntPtr image); + public static void XISetMask(ref int mask, XiEventType ev) { mask |= (1 << (int)ev); From a832d639155ea05b84d35fa17a48c32b1e0366b1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 20 Oct 2020 13:46:47 +0200 Subject: [PATCH 023/164] IStandardCursorFactory -> ICursorFactory. --- .../Remote/PreviewerWindowingPlatform.cs | 2 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 2 +- src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs | 2 +- src/Avalonia.Headless/HeadlessPlatformStubs.cs | 2 +- src/Avalonia.Input/Cursor.cs | 4 ++-- .../{IStandardCursorFactory.cs => ICursorFactory.cs} | 2 +- src/Avalonia.Native/AvaloniaNativePlatform.cs | 2 +- src/Avalonia.Native/Cursor.cs | 2 +- src/Avalonia.Native/WindowImplBase.cs | 4 ++-- src/Avalonia.X11/X11CursorFactory.cs | 2 +- src/Avalonia.X11/X11Platform.cs | 2 +- .../Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs | 2 +- src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs | 2 +- src/Windows/Avalonia.Win32/CursorFactory.cs | 2 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 2 +- .../CalendarDatePickerTests.cs | 2 +- tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs | 2 +- tests/Avalonia.Controls.UnitTests/DatePickerTests.cs | 2 +- tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs | 4 ++-- tests/Avalonia.Controls.UnitTests/TextBoxTests.cs | 4 ++-- .../TextBoxTests_DataValidation.cs | 2 +- tests/Avalonia.Controls.UnitTests/TimePickerTests.cs | 2 +- tests/Avalonia.UnitTests/TestServices.cs | 8 ++++---- tests/Avalonia.UnitTests/UnitTestApplication.cs | 2 +- 24 files changed, 31 insertions(+), 31 deletions(-) rename src/Avalonia.Input/Platform/{IStandardCursorFactory.cs => ICursorFactory.cs} (83%) diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index fe4c580bbb..67b832318a 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -47,7 +47,7 @@ namespace Avalonia.DesignerSupport.Remote var threading = new InternalPlatformThreadingInterface(); AvaloniaLocator.CurrentMutable .Bind().ToSingleton() - .Bind().ToSingleton() + .Bind().ToSingleton() .Bind().ToConstant(Keyboard) .Bind().ToConstant(instance) .Bind().ToConstant(threading) diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index a400102877..df1e5d6e98 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -192,7 +192,7 @@ namespace Avalonia.DesignerSupport.Remote public Task GetDataAsync(string format) => Task.FromResult((object)null); } - class CursorFactoryStub : IStandardCursorFactory + class CursorFactoryStub : ICursorFactory { public IPlatformHandle GetCursor(StandardCursorType cursorType) => new PlatformHandle(IntPtr.Zero, "STUB"); public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new PlatformHandle(IntPtr.Zero, "STUB"); diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index 1f750a0309..fca2a1336f 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -58,7 +58,7 @@ namespace Avalonia.Headless AvaloniaLocator.CurrentMutable .Bind().ToConstant(new HeadlessPlatformThreadingInterface()) .Bind().ToSingleton() - .Bind().ToSingleton() + .Bind().ToSingleton() .Bind().ToConstant(new HeadlessPlatformSettingsStub()) .Bind().ToSingleton() .Bind().ToSingleton() diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index 43e6731eee..efad5133b9 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -52,7 +52,7 @@ namespace Avalonia.Headless } } - class HeadlessCursorFactoryStub : IStandardCursorFactory + class HeadlessCursorFactoryStub : ICursorFactory { public IPlatformHandle GetCursor(StandardCursorType cursorType) { diff --git a/src/Avalonia.Input/Cursor.cs b/src/Avalonia.Input/Cursor.cs index 21c2b1a326..f018041a94 100644 --- a/src/Avalonia.Input/Cursor.cs +++ b/src/Avalonia.Input/Cursor.cs @@ -77,9 +77,9 @@ namespace Avalonia.Input throw new ArgumentException($"Unrecognized cursor type '{s}'."); } - private static IStandardCursorFactory GetCursorFactory() + private static ICursorFactory GetCursorFactory() { - return AvaloniaLocator.Current.GetService() ?? + return AvaloniaLocator.Current.GetService() ?? throw new Exception("Could not create Cursor: ICursorFactory not registered."); } } diff --git a/src/Avalonia.Input/Platform/IStandardCursorFactory.cs b/src/Avalonia.Input/Platform/ICursorFactory.cs similarity index 83% rename from src/Avalonia.Input/Platform/IStandardCursorFactory.cs rename to src/Avalonia.Input/Platform/ICursorFactory.cs index 12c3ec7b74..e531015d40 100644 --- a/src/Avalonia.Input/Platform/IStandardCursorFactory.cs +++ b/src/Avalonia.Input/Platform/ICursorFactory.cs @@ -4,7 +4,7 @@ using Avalonia.Input; namespace Avalonia.Platform { - public interface IStandardCursorFactory + public interface ICursorFactory { IPlatformHandle GetCursor(StandardCursorType cursorType); IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot); diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index e8b2f065c7..c56a8404fa 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -99,7 +99,7 @@ namespace Avalonia.Native AvaloniaLocator.CurrentMutable .Bind() .ToConstant(new PlatformThreadingInterface(_factory.CreatePlatformThreadingInterface())) - .Bind().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) + .Bind().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) .Bind().ToSingleton() .Bind().ToConstant(KeyboardDevice) .Bind().ToConstant(this) diff --git a/src/Avalonia.Native/Cursor.cs b/src/Avalonia.Native/Cursor.cs index fffff51bce..ebee4baafe 100644 --- a/src/Avalonia.Native/Cursor.cs +++ b/src/Avalonia.Native/Cursor.cs @@ -24,7 +24,7 @@ namespace Avalonia.Native } } - class CursorFactory : IStandardCursorFactory + class CursorFactory : ICursorFactory { IAvnCursorFactory _native; diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 5d35c773d7..e4643bf675 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -53,7 +53,7 @@ namespace Avalonia.Native private bool _gpu = false; private readonly MouseDevice _mouse; private readonly IKeyboardDevice _keyboard; - private readonly IStandardCursorFactory _cursorFactory; + private readonly ICursorFactory _cursorFactory; private Size _savedLogicalSize; private Size _lastRenderedLogicalSize; private double _savedScaling; @@ -68,7 +68,7 @@ namespace Avalonia.Native _keyboard = AvaloniaLocator.Current.GetService(); _mouse = new MouseDevice(); - _cursorFactory = AvaloniaLocator.Current.GetService(); + _cursorFactory = AvaloniaLocator.Current.GetService(); } protected void Init(IAvnWindowBase window, IAvnScreens screens, IGlContext glContext) diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 176878ed5f..7f08407239 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -11,7 +11,7 @@ using Avalonia.Utilities; namespace Avalonia.X11 { - class X11CursorFactory : IStandardCursorFactory + class X11CursorFactory : ICursorFactory { private static readonly byte[] NullCursorData = new byte[] { 0 }; diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index c6db146f7b..ffd7c75608 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -52,7 +52,7 @@ namespace Avalonia.X11 .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)) .Bind().ToFunc(() => KeyboardDevice) - .Bind().ToConstant(new X11CursorFactory(Display)) + .Bind().ToConstant(new X11CursorFactory(Display)) .Bind().ToConstant(new X11Clipboard(this)) .Bind().ToConstant(new PlatformSettingsStub()) .Bind().ToConstant(new X11IconLoader(Info)) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 8801f71f9a..340d10517d 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -38,7 +38,7 @@ namespace Avalonia.LinuxFramebuffer .Bind().ToConstant(Threading) .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new RenderLoop()) - .Bind().ToTransient() + .Bind().ToTransient() .Bind().ToConstant(new KeyboardDevice()) .Bind().ToSingleton() .Bind().ToConstant(new RenderLoop()) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs index 8763f18cc0..def5f6b817 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs @@ -4,7 +4,7 @@ using Avalonia.Platform; namespace Avalonia.LinuxFramebuffer { - internal class CursorFactoryStub : IStandardCursorFactory + internal class CursorFactoryStub : ICursorFactory { public IPlatformHandle GetCursor(StandardCursorType cursorType) { diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index 844f230a2e..aca5f42771 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -13,7 +13,7 @@ using SdPixelFormat = System.Drawing.Imaging.PixelFormat; namespace Avalonia.Win32 { - internal class CursorFactory : IStandardCursorFactory + internal class CursorFactory : ICursorFactory { public static CursorFactory Instance { get; } = new CursorFactory(); diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 61c1a4f45e..ebc69072fc 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -90,7 +90,7 @@ namespace Avalonia.Win32 Options = options; AvaloniaLocator.CurrentMutable .Bind().ToSingleton() - .Bind().ToConstant(CursorFactory.Instance) + .Bind().ToConstant(CursorFactory.Instance) .Bind().ToConstant(WindowsKeyboardDevice.Instance) .Bind().ToConstant(s_instance) .Bind().ToConstant(s_instance) diff --git a/tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs index f41a3e7581..d77c7b87fa 100644 --- a/tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs @@ -74,7 +74,7 @@ namespace Avalonia.Controls.UnitTests } private static TestServices Services => TestServices.MockThreadingInterface.With( - standardCursorFactory: Mock.Of()); + standardCursorFactory: Mock.Of()); private CalendarDatePicker CreateControl() { diff --git a/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs b/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs index 66c6033fb4..0c5a395ba4 100644 --- a/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs +++ b/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs @@ -4,7 +4,7 @@ using Avalonia.Platform; namespace Avalonia.Controls.UnitTests { - public class CursorFactoryMock : IStandardCursorFactory + public class CursorFactoryMock : ICursorFactory { public IPlatformHandle GetCursor(StandardCursorType cursorType) { diff --git a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs index 7bcb120850..3105263290 100644 --- a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs @@ -204,7 +204,7 @@ namespace Avalonia.Controls.UnitTests private static TestServices Services => TestServices.MockThreadingInterface.With( fontManagerImpl: new MockFontManagerImpl(), - standardCursorFactory: Mock.Of(), + standardCursorFactory: Mock.Of(), textShaperImpl: new MockTextShaperImpl()); private IControlTemplate CreateTemplate() diff --git a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs index f2b6b0db4b..72ccead783 100644 --- a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs @@ -11,8 +11,8 @@ namespace Avalonia.Controls.UnitTests { public GridSplitterTests() { - var cursorFactoryImpl = new Mock(); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(cursorFactoryImpl.Object); + var cursorFactoryImpl = new Mock(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(cursorFactoryImpl.Object); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index fe25fa7346..6ac7799828 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -698,10 +698,10 @@ namespace Avalonia.Controls.UnitTests keyboardDevice: () => new KeyboardDevice(), keyboardNavigation: new KeyboardNavigationHandler(), inputManager: new InputManager(), - standardCursorFactory: Mock.Of()); + standardCursorFactory: Mock.Of()); private static TestServices Services => TestServices.MockThreadingInterface.With( - standardCursorFactory: Mock.Of()); + standardCursorFactory: Mock.Of()); private IControlTemplate CreateTemplate() { diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs index 78b6bf01ec..570b9ee4ea 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs @@ -86,7 +86,7 @@ namespace Avalonia.Controls.UnitTests } private static TestServices Services => TestServices.MockThreadingInterface.With( - standardCursorFactory: Mock.Of()); + standardCursorFactory: Mock.Of()); private IControlTemplate CreateTemplate() { diff --git a/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs index 682f0eaadb..c39a963dae 100644 --- a/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs @@ -100,7 +100,7 @@ namespace Avalonia.Controls.UnitTests private static TestServices Services => TestServices.MockThreadingInterface.With( fontManagerImpl: new MockFontManagerImpl(), - standardCursorFactory: Mock.Of(), + standardCursorFactory: Mock.Of(), textShaperImpl: new MockTextShaperImpl()); private IControlTemplate CreateTemplate() diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 012cab23dc..8d27562146 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -23,7 +23,7 @@ namespace Avalonia.UnitTests assetLoader: new AssetLoader(), platform: new AppBuilder().RuntimePlatform, renderInterface: new MockPlatformRenderInterface(), - standardCursorFactory: Mock.Of(), + standardCursorFactory: Mock.Of(), styler: new Styler(), theme: () => CreateDefaultTheme(), threadingInterface: Mock.Of(x => x.CurrentThreadIsLoopThread == true), @@ -70,7 +70,7 @@ namespace Avalonia.UnitTests IPlatformRenderInterface renderInterface = null, IRenderTimer renderLoop = null, IScheduler scheduler = null, - IStandardCursorFactory standardCursorFactory = null, + ICursorFactory standardCursorFactory = null, IStyler styler = null, Func theme = null, IPlatformThreadingInterface threadingInterface = null, @@ -111,7 +111,7 @@ namespace Avalonia.UnitTests public IFontManagerImpl FontManagerImpl { get; } public ITextShaperImpl TextShaperImpl { get; } public IScheduler Scheduler { get; } - public IStandardCursorFactory StandardCursorFactory { get; } + public ICursorFactory StandardCursorFactory { get; } public IStyler Styler { get; } public Func Theme { get; } public IPlatformThreadingInterface ThreadingInterface { get; } @@ -130,7 +130,7 @@ namespace Avalonia.UnitTests IPlatformRenderInterface renderInterface = null, IRenderTimer renderLoop = null, IScheduler scheduler = null, - IStandardCursorFactory standardCursorFactory = null, + ICursorFactory standardCursorFactory = null, IStyler styler = null, Func theme = null, IPlatformThreadingInterface threadingInterface = null, diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index e4a65f105d..6fc82088e9 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -62,7 +62,7 @@ namespace Avalonia.UnitTests .Bind().ToConstant(Services.TextShaperImpl) .Bind().ToConstant(Services.ThreadingInterface) .Bind().ToConstant(Services.Scheduler) - .Bind().ToConstant(Services.StandardCursorFactory) + .Bind().ToConstant(Services.StandardCursorFactory) .Bind().ToConstant(Services.Styler) .Bind().ToConstant(Services.WindowingPlatform) .Bind().ToSingleton(); From ca408e55b5091449158cfae7536960464389f6fd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 20 Oct 2020 14:32:28 +0200 Subject: [PATCH 024/164] Added ICursorImpl. --- .../Offscreen/OffscreenTopLevelImpl.cs | 2 +- .../Platform/ITopLevelImpl.cs | 2 +- src/Avalonia.Controls/TopLevel.cs | 2 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 11 ++-- .../HeadlessPlatformStubs.cs | 10 ++-- src/Avalonia.Headless/HeadlessWindowImpl.cs | 2 +- src/Avalonia.Input/Cursor.cs | 6 +-- src/Avalonia.Input/Platform/ICursorFactory.cs | 4 +- src/Avalonia.Input/Platform/ICursorImpl.cs | 14 +++++ src/Avalonia.Native/Cursor.cs | 6 +-- src/Avalonia.Native/WindowImplBase.cs | 2 +- src/Avalonia.X11/X11CursorFactory.cs | 19 ++++--- src/Avalonia.X11/X11Window.cs | 8 ++- .../FramebufferToplevelImpl.cs | 2 +- src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs | 10 ++-- .../Wpf/WpfTopLevelImpl.cs | 6 +-- src/Windows/Avalonia.Win32/CursorFactory.cs | 52 ++++++++++++------- .../Interop/UnmanagedMethods.cs | 3 ++ src/Windows/Avalonia.Win32/WindowImpl.cs | 15 ++++-- .../CursorFactoryMock.cs | 16 ++++-- 20 files changed, 121 insertions(+), 71 deletions(-) create mode 100644 src/Avalonia.Input/Platform/ICursorImpl.cs diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 522103c7bd..ca0e9d48b8 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -61,7 +61,7 @@ namespace Avalonia.Controls.Embedding.Offscreen public virtual PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, 1); - public virtual void SetCursor(IPlatformHandle cursor) + public virtual void SetCursor(ICursorImpl cursor) { } diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 7514f214aa..09f38042a1 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -98,7 +98,7 @@ namespace Avalonia.Platform /// Sets the cursor associated with the toplevel. /// /// The cursor. Use null for default cursor - void SetCursor(IPlatformHandle cursor); + void SetCursor(ICursorImpl cursor); /// /// Gets or sets a method called when the underlying implementation is destroyed. diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 3d24f60463..ffccdeb3e2 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -162,7 +162,7 @@ namespace Avalonia.Controls this.GetObservable(PointerOverElementProperty) .Select( x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty()) - .Switch().Subscribe(cursor => PlatformImpl?.SetCursor(cursor?.PlatformCursor)); + .Switch().Subscribe(cursor => PlatformImpl?.SetCursor(cursor?.PlatformImpl)); if (((IStyleHost)this).StylingParent is IResourceHost applicationResources) { diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index df1e5d6e98..751c5c08f2 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -73,7 +73,7 @@ namespace Avalonia.DesignerSupport.Remote public PixelPoint PointToScreen(Point p) => PixelPoint.FromPoint(p, 1); - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { } @@ -194,8 +194,13 @@ namespace Avalonia.DesignerSupport.Remote class CursorFactoryStub : ICursorFactory { - public IPlatformHandle GetCursor(StandardCursorType cursorType) => new PlatformHandle(IntPtr.Zero, "STUB"); - public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new PlatformHandle(IntPtr.Zero, "STUB"); + public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub(); + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub(); + + private class CursorStub : ICursorImpl + { + public void Dispose() { } + } } class IconLoaderStub : IPlatformIconLoader diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index efad5133b9..ce4c31e27e 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -54,14 +54,12 @@ namespace Avalonia.Headless class HeadlessCursorFactoryStub : ICursorFactory { - public IPlatformHandle GetCursor(StandardCursorType cursorType) - { - return new PlatformHandle(new IntPtr((int)cursorType), "STUB"); - } + public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub(); + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub(); - public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + private class CursorStub : ICursorImpl { - return new PlatformHandle(IntPtr.Zero, "STUB"); + public void Dispose() { } } } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index 8f4fa5e304..2c52438743 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -67,7 +67,7 @@ namespace Avalonia.Headless public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, RenderScaling); - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { } diff --git a/src/Avalonia.Input/Cursor.cs b/src/Avalonia.Input/Cursor.cs index f018041a94..052ef5e9a0 100644 --- a/src/Avalonia.Input/Cursor.cs +++ b/src/Avalonia.Input/Cursor.cs @@ -53,9 +53,9 @@ namespace Avalonia.Input { public static readonly Cursor Default = new Cursor(StandardCursorType.Arrow); - internal Cursor(IPlatformHandle platformCursor) + internal Cursor(ICursorImpl platformImpl) { - PlatformCursor = platformCursor; + PlatformImpl = platformImpl; } public Cursor(StandardCursorType cursorType) @@ -68,7 +68,7 @@ namespace Avalonia.Input { } - public IPlatformHandle PlatformCursor { get; } + public ICursorImpl PlatformImpl { get; } public static Cursor Parse(string s) { diff --git a/src/Avalonia.Input/Platform/ICursorFactory.cs b/src/Avalonia.Input/Platform/ICursorFactory.cs index e531015d40..fff1f92d53 100644 --- a/src/Avalonia.Input/Platform/ICursorFactory.cs +++ b/src/Avalonia.Input/Platform/ICursorFactory.cs @@ -6,7 +6,7 @@ namespace Avalonia.Platform { public interface ICursorFactory { - IPlatformHandle GetCursor(StandardCursorType cursorType); - IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot); + ICursorImpl GetCursor(StandardCursorType cursorType); + ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot); } } diff --git a/src/Avalonia.Input/Platform/ICursorImpl.cs b/src/Avalonia.Input/Platform/ICursorImpl.cs new file mode 100644 index 0000000000..14235869f7 --- /dev/null +++ b/src/Avalonia.Input/Platform/ICursorImpl.cs @@ -0,0 +1,14 @@ +using System; +using Avalonia.Input; + +#nullable enable + +namespace Avalonia.Platform +{ + /// + /// Represents a platform implementation of a . + /// + public interface ICursorImpl : IDisposable + { + } +} diff --git a/src/Avalonia.Native/Cursor.cs b/src/Avalonia.Native/Cursor.cs index ebee4baafe..2cb085bc7e 100644 --- a/src/Avalonia.Native/Cursor.cs +++ b/src/Avalonia.Native/Cursor.cs @@ -5,7 +5,7 @@ using Avalonia.Native.Interop; namespace Avalonia.Native { - class AvaloniaNativeCursor : IPlatformHandle, IDisposable + class AvaloniaNativeCursor : ICursorImpl, IDisposable { public IAvnCursor Cursor { get; private set; } public IntPtr Handle => IntPtr.Zero; @@ -33,13 +33,13 @@ namespace Avalonia.Native _native = native; } - public IPlatformHandle GetCursor(StandardCursorType cursorType) + public ICursorImpl GetCursor(StandardCursorType cursorType) { var cursor = _native.GetCursor((AvnStandardCursorType)cursorType); return new AvaloniaNativeCursor( cursor ); } - public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index e4643bf675..ad2b7411eb 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -389,7 +389,7 @@ namespace Avalonia.Native public Action Deactivated { get; set; } public Action Activated { get; set; } - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { if (_native == null) { diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 7f08407239..f95d4320fe 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -56,7 +56,7 @@ namespace Avalonia.X11 .ToDictionary(id => id, id => XLib.XCreateFontCursor(_display, id)); } - public IPlatformHandle GetCursor(StandardCursorType cursorType) + public ICursorImpl GetCursor(StandardCursorType cursorType) { IntPtr handle; if (cursorType == StandardCursorType.None) @@ -69,10 +69,10 @@ namespace Avalonia.X11 ? _cursors[shape] : _cursors[CursorFontShape.XC_top_left_arrow]; } - return new PlatformHandle(handle, "XCURSOR"); + return new CursorImpl(handle); } - public unsafe IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + public unsafe ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) { return new XImageCursor(_display, cursor, hotSpot); } @@ -85,7 +85,7 @@ namespace Avalonia.X11 return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); } - private unsafe class XImageCursor : IFramebufferPlatformSurface, IPlatformHandle, IDisposable + private unsafe class XImageCursor : CursorImpl, IFramebufferPlatformSurface, IPlatformHandle { private readonly PixelSize _pixelSize; private readonly IUnmanagedBlob _blob; @@ -117,10 +117,9 @@ namespace Avalonia.X11 Handle = XLib.XcursorImageLoadCursor(display, _blob.Address); } - public IntPtr Handle { get; } public string HandleDescriptor => "XCURSOR"; - public void Dispose() + public override void Dispose() { XLib.XcursorImageDestroy(Handle); _blob.Dispose(); @@ -135,4 +134,12 @@ namespace Avalonia.X11 } } } + + class CursorImpl : ICursorImpl + { + public CursorImpl() { } + public CursorImpl(IntPtr handle) => Handle = handle; + public IntPtr Handle { get; protected set; } + public virtual void Dispose() { } + } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 2cd3b973d8..32c455571f 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -878,15 +878,13 @@ namespace Avalonia.X11 UpdateSizeHints(null); } - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { if (cursor == null) XDefineCursor(_x11.Display, _handle, _x11.DefaultCursor); - else + else if (cursor is CursorImpl impl) { - if (cursor.HandleDescriptor != "XCURSOR") - throw new ArgumentException("Expected XCURSOR handle type"); - XDefineCursor(_x11.Display, _handle, cursor.Handle); + XDefineCursor(_x11.Display, _handle, impl.Handle); } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 0a101eec7a..4bbb58e53e 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -57,7 +57,7 @@ namespace Avalonia.LinuxFramebuffer public PixelPoint PointToScreen(Point p) => PixelPoint.FromPoint(p, 1); - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs index def5f6b817..642be28c69 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs @@ -6,14 +6,12 @@ namespace Avalonia.LinuxFramebuffer { internal class CursorFactoryStub : ICursorFactory { - public IPlatformHandle GetCursor(StandardCursorType cursorType) - { - return new PlatformHandle(IntPtr.Zero, null); - } + public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub(); + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub(); - public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + private class CursorStub : ICursorImpl { - return new PlatformHandle(IntPtr.Zero, null); + public void Dispose() { } } } internal class PlatformSettings : IPlatformSettings diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 3467a33d16..3bb29f4e23 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -225,12 +225,12 @@ namespace Avalonia.Win32.Interop.Wpf protected override void OnTextInput(TextCompositionEventArgs e) => _ttl.Input?.Invoke(new RawTextInputEventArgs(_keyboard, (uint) e.Timestamp, _inputRoot, e.Text)); - void ITopLevelImpl.SetCursor(IPlatformHandle cursor) + void ITopLevelImpl.SetCursor(ICursorImpl cursor) { if (cursor == null) Cursor = Cursors.Arrow; - else if (cursor.HandleDescriptor == "HCURSOR") - Cursor = CursorShim.FromHCursor(cursor.Handle); + else if (cursor is IPlatformHandle handle) + Cursor = CursorShim.FromHCursor(handle.Handle); } Action ITopLevelImpl.Input { get; set; } //TODO diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index aca5f42771..10878ba7b5 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -3,9 +3,7 @@ using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.IO; -using System.Runtime.InteropServices; using Avalonia.Input; -using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Win32.Interop; using SdBitmap = System.Drawing.Bitmap; @@ -36,8 +34,7 @@ namespace Avalonia.Win32 IntPtr cursor = UnmanagedMethods.LoadCursor(mh, new IntPtr(id)); if (cursor != IntPtr.Zero) { - PlatformHandle phCursor = new PlatformHandle(cursor, PlatformConstants.CursorHandleType); - Cache.Add(cursorType, phCursor); + Cache.Add(cursorType, new CursorImpl(cursor, false)); } } } @@ -77,25 +74,23 @@ namespace Avalonia.Win32 {StandardCursorType.DragLink, 32516}, }; - private static readonly Dictionary Cache = - new Dictionary(); + private static readonly Dictionary Cache = + new Dictionary(); - public IPlatformHandle GetCursor(StandardCursorType cursorType) + public ICursorImpl GetCursor(StandardCursorType cursorType) { - IPlatformHandle rv; - if (!Cache.TryGetValue(cursorType, out rv)) + if (!Cache.TryGetValue(cursorType, out var rv)) { - Cache[cursorType] = - rv = - new PlatformHandle( - UnmanagedMethods.LoadCursor(IntPtr.Zero, new IntPtr(CursorTypeMapping[cursorType])), - PlatformConstants.CursorHandleType); + rv = new CursorImpl( + UnmanagedMethods.LoadCursor(IntPtr.Zero, new IntPtr(CursorTypeMapping[cursorType])), + false); + Cache.Add(cursorType, rv); } return rv; } - public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) { using var source = LoadSystemDrawingBitmap(cursor); using var mask = AlphaToMask(source); @@ -109,9 +104,7 @@ namespace Avalonia.Win32 ColorBitmap = source.GetHbitmap(), }; - return new PlatformHandle( - UnmanagedMethods.CreateIconIndirect(ref info), - PlatformConstants.CursorHandleType); + return new CursorImpl(UnmanagedMethods.CreateIconIndirect(ref info), true); } private SdBitmap LoadSystemDrawingBitmap(IBitmapImpl bitmap) @@ -168,4 +161,27 @@ namespace Avalonia.Win32 } } } + + internal class CursorImpl : ICursorImpl, IPlatformHandle + { + private readonly bool _isCustom; + + public CursorImpl(IntPtr handle, bool isCustom) + { + Handle = handle; + _isCustom = isCustom; + } + + public IntPtr Handle { get; private set; } + public string HandleDescriptor => PlatformConstants.CursorHandleType; + + public void Dispose() + { + if (_isCustom && Handle != IntPtr.Zero) + { + UnmanagedMethods.DestroyIcon(Handle); + Handle = IntPtr.Zero; + } + } + } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index c18a5c07c5..3b11aecc2d 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1034,6 +1034,9 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern IntPtr CreateIconIndirect([In] ref ICONINFO iconInfo); + [DllImport("user32.dll")] + public static extern bool DestroyIcon(IntPtr hIcon); + [DllImport("user32.dll")] public static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 7079a0120c..755570bff1 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -557,14 +557,19 @@ namespace Avalonia.Win32 SetWindowText(_hwnd, title); } - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { - var hCursor = cursor?.Handle ?? DefaultCursor; - SetClassLong(_hwnd, ClassLongIndex.GCLP_HCURSOR, hCursor); + var impl = cursor as CursorImpl; - if (_owner.IsPointerOver) + if (cursor is null || impl is object) { - UnmanagedMethods.SetCursor(hCursor); + var hCursor = impl?.Handle ?? DefaultCursor; + SetClassLong(_hwnd, ClassLongIndex.GCLP_HCURSOR, hCursor); + + if (_owner.IsPointerOver) + { + UnmanagedMethods.SetCursor(hCursor); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs b/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs index 0c5a395ba4..ee4264e6b9 100644 --- a/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs +++ b/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs @@ -1,4 +1,3 @@ -using System; using Avalonia.Input; using Avalonia.Platform; @@ -6,14 +5,21 @@ namespace Avalonia.Controls.UnitTests { public class CursorFactoryMock : ICursorFactory { - public IPlatformHandle GetCursor(StandardCursorType cursorType) + public ICursorImpl GetCursor(StandardCursorType cursorType) { - return new PlatformHandle(IntPtr.Zero, cursorType.ToString()); + return new MockCursorImpl(); } - public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) { - return new PlatformHandle(IntPtr.Zero, "Custom"); + return new MockCursorImpl(); + } + + private class MockCursorImpl : ICursorImpl + { + public void Dispose() + { + } } } } From f0d097ed1234a9a45ab42451c52bba8afc4ac37d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 20 Oct 2020 14:34:50 +0200 Subject: [PATCH 025/164] Make Cursor disposable. --- src/Avalonia.Input/Cursor.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Input/Cursor.cs b/src/Avalonia.Input/Cursor.cs index 052ef5e9a0..2b99c51472 100644 --- a/src/Avalonia.Input/Cursor.cs +++ b/src/Avalonia.Input/Cursor.cs @@ -6,13 +6,6 @@ using Avalonia.Platform; namespace Avalonia.Input { - /* - ========================================================================================= - NOTE: Cursors are NOT disposable and are cached in platform implementation. - To support loading custom cursors some measures about that should be taken beforehand - ========================================================================================= - */ - public enum StandardCursorType { Arrow, @@ -49,7 +42,7 @@ namespace Avalonia.Input // SizeNorthEastSouthWest, } - public class Cursor + public class Cursor : IDisposable { public static readonly Cursor Default = new Cursor(StandardCursorType.Arrow); @@ -70,6 +63,8 @@ namespace Avalonia.Input public ICursorImpl PlatformImpl { get; } + public void Dispose() => PlatformImpl.Dispose(); + public static Cursor Parse(string s) { return Enum.TryParse(s, true, out var t) ? From 04f5d3a252f1c029ccc64f16684c07eb8cd6c798 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 20 Oct 2020 14:38:01 +0200 Subject: [PATCH 026/164] Don't allow premultipied alpha for the moment. --- src/Windows/Avalonia.Win32/CursorFactory.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index 10878ba7b5..0a0e45b03a 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -118,8 +118,13 @@ namespace Avalonia.Win32 { var dest = new SdBitmap(source.Width, source.Height, SdPixelFormat.Format1bppIndexed); - if (source.PixelFormat != SdPixelFormat.Format32bppArgb && - source.PixelFormat != SdPixelFormat.Format32bppPArgb) + if (source.PixelFormat == SdPixelFormat.Format32bppPArgb) + { + throw new NotSupportedException( + "Images with premultiplied alpha not yet supported as cursor images."); + } + + if (source.PixelFormat != SdPixelFormat.Format32bppArgb) { return dest; } From 98974af13e7e08662c32b4744e6df9f89490d97c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 21 Oct 2020 14:44:24 +0200 Subject: [PATCH 027/164] Added cursor page to control catalog. --- samples/ControlCatalog/Assets/avalonia-32.png | Bin 0 -> 1322 bytes samples/ControlCatalog/MainView.xaml | 4 ++ samples/ControlCatalog/Pages/CursorPage.xaml | 29 ++++++++++++ .../ControlCatalog/Pages/CursorPage.xaml.cs | 20 +++++++++ .../ViewModels/CursorPageViewModel.cs | 42 ++++++++++++++++++ 5 files changed, 95 insertions(+) create mode 100644 samples/ControlCatalog/Assets/avalonia-32.png create mode 100644 samples/ControlCatalog/Pages/CursorPage.xaml create mode 100644 samples/ControlCatalog/Pages/CursorPage.xaml.cs create mode 100644 samples/ControlCatalog/ViewModels/CursorPageViewModel.cs diff --git a/samples/ControlCatalog/Assets/avalonia-32.png b/samples/ControlCatalog/Assets/avalonia-32.png new file mode 100644 index 0000000000000000000000000000000000000000..7b443e7a250846dc898e6467cddeed8c55880766 GIT binary patch literal 1322 zcmV+_1=aeAP)A zL5V<&1`?F`W{`LZcsE5cBpMzF4;mv6L;;~9STsnu#G;m4u_XwFBGRT;C>`6GUgn(T zL)$4c=S({l;{UQ|_FDV=|6Y6Tb=DCggsQrL1;9dJ29OIR^qV_@TA&g*4xAK`-bkN4 z9C5m;mZ)ljMvf*`-Kwf%?}yk>)mK&3GXmmJbg1eZ{m~l{d7`S8-Uo3gep1zxNXaLv zYSjZIAHbigniI+=A>>IwIZ!Z^$o9rgT58+rX!W5AiD_}9PaZ`|X5w)By8#r7NUKeN zsv5v|z%r+mmfzx!BR9Bs_y#St?T$}Rn>32TB~y5G>114PBbTZ98d$;Vp#KA9s55xh1snb zVql0XA`$Lr7fUu3F?C*+li3I4iAdc*oHjU-H`Lr_?|aAS_Vr+?iKSLxRCGuI{vMMb zK0ZUk2-=%=h?IY zi`~EzPRh&oS7NDYn_skkCi9*z44TU>OeecAopT4SaeU`R+nUNljsgG{8T7qa0_gPk zXlwS6nU~go-h*d9ZQcYM`MsF6BA5Jy*|vF4lb7b}cb%5ZFn}oo|JrZ=!;Uw1vwh<( zW-ZO<@uv%dM9f`1WPJjlXysJL`Q~em0&)x>8Nl1o#aATvT{Op4xk1SOI5BuQ<-fC&z>p5V$uYLPByp$*GA{ zp16$P^aoYHvq!3^ID9EAdG)bc&K+_VkT53JX<7~XdN>>AE`KbD+)@+MviN388T+;# zrTOMyiM?Id&f(9>`L^T)rsa?TLqJqD7H9y1I+mWUUfx>%DV|nu*isxDAA`$q)9W*X zY)MfmgjVRHRe3B}Z@b2pAtGJC?odl~Y!ok+tVCScA~7&_dAsp-`bPYsx)%e5ejCso zYRoU1!kTxMgP~zX432}pM*;&tL~atM8JV+eCL4CFC1G@I#3CZ87oiaV_z*bhHmSh z$FX-7ktX^Yl}~^N^O5O{ABXi&)f`p*LRC8-@RyQDRdXYjs;Wt<`dq)5v#Q$iA4n}# g?NZe;Rhb%7 literal 0 HcmV?d00001 diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index bd5beafe29..34ff4b1a54 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -22,6 +22,10 @@ + + + diff --git a/samples/ControlCatalog/Pages/CursorPage.xaml b/samples/ControlCatalog/Pages/CursorPage.xaml new file mode 100644 index 0000000000..a28039ea3f --- /dev/null +++ b/samples/ControlCatalog/Pages/CursorPage.xaml @@ -0,0 +1,29 @@ + + + + Cursor + Defines a cursor (mouse pointer) + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/CursorPage.xaml.cs b/samples/ControlCatalog/Pages/CursorPage.xaml.cs new file mode 100644 index 0000000000..9e9e9ba8b9 --- /dev/null +++ b/samples/ControlCatalog/Pages/CursorPage.xaml.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public class CursorPage : UserControl + { + public CursorPage() + { + this.InitializeComponent(); + DataContext = new CursorPageViewModel(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/ViewModels/CursorPageViewModel.cs b/samples/ControlCatalog/ViewModels/CursorPageViewModel.cs new file mode 100644 index 0000000000..a4c643dd04 --- /dev/null +++ b/samples/ControlCatalog/ViewModels/CursorPageViewModel.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia; +using Avalonia.Input; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using ReactiveUI; + +namespace ControlCatalog.ViewModels +{ + public class CursorPageViewModel : ReactiveObject + { + public CursorPageViewModel() + { + StandardCursors = Enum.GetValues(typeof(StandardCursorType)) + .Cast() + .Select(x => new StandardCursorModel(x)) + .ToList(); + + var loader = AvaloniaLocator.Current.GetService(); + var s = loader.Open(new Uri("avares://ControlCatalog/Assets/avalonia-32.png")); + var bitmap = new Bitmap(s); + CustomCursor = new Cursor(bitmap, new PixelPoint(16, 16)); + } + + public IEnumerable StandardCursors { get; } + public Cursor CustomCursor { get; } + + public class StandardCursorModel + { + public StandardCursorModel(StandardCursorType type) + { + Type = type; + Cursor = new Cursor(type); + } + + public StandardCursorType Type { get; } + public Cursor Cursor { get; } + } + } +} From 41c4630bbb2fac9e9247f864d510079a9e60327b Mon Sep 17 00:00:00 2001 From: Adir Date: Fri, 23 Oct 2020 14:08:05 +0300 Subject: [PATCH 028/164] Changed if statements to have braces Moved comments above if --- src/Avalonia.Controls/Button.cs | 8 +++++--- src/Avalonia.Controls/MenuItem.cs | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 87fabf5ed7..9cd7a19e7e 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -208,9 +208,11 @@ 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 + if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control + { HotKey = _hotkey; + } + base.OnAttachedToLogicalTree(e); if (Command != null) @@ -221,9 +223,9 @@ 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 if (HotKey != null) { - // This will cause the hotkey manager to dispose the observer and the reference to this control _hotkey = HotKey; HotKey = null; } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index faf92c97fe..8612f696bc 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -338,9 +338,11 @@ 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 + if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control + { HotKey = _hotkey; + } + base.OnAttachedToLogicalTree(e); if (Command != null) @@ -351,9 +353,9 @@ 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 if (HotKey != null) { - // This will cause the hotkey manager to dispose the observer and the reference to this control _hotkey = HotKey; HotKey = null; } From 1b0ff07133851b32f853d6815a7cc17612475f0e Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 13 Nov 2020 13:21:23 +0100 Subject: [PATCH 029/164] Fixes Issue 4980: Message loop exit when GetMessage < 0 --- src/Avalonia.Base/Logging/LogArea.cs | 5 +++++ .../Avalonia.Win32/Interop/UnmanagedMethods.cs | 4 ++-- src/Windows/Avalonia.Win32/Win32Platform.cs | 11 ++++++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs index 3c19b47a05..ce330247f3 100644 --- a/src/Avalonia.Base/Logging/LogArea.cs +++ b/src/Avalonia.Base/Logging/LogArea.cs @@ -34,5 +34,10 @@ namespace Avalonia.Logging /// The log event comes from the control system. /// public const string Control = "Control"; + + /// + /// The log evevnt come from Win32Platform + /// + public const string Win32Platform = nameof(Win32Platform); } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index f88c57cf59..85938829f6 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -931,8 +931,8 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll", EntryPoint = "MapVirtualKeyW")] public static extern uint MapVirtualKey(uint uCode, uint uMapType); - [DllImport("user32.dll", EntryPoint = "GetMessageW")] - public static extern sbyte GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); + [DllImport("user32.dll", EntryPoint = "GetMessageW",SetLastError = true)] + public static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); [DllImport("user32.dll")] public static extern int GetMessageTime(); diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 5b16cae26e..209cb63603 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -137,13 +137,18 @@ namespace Avalonia.Win32 public void RunLoop(CancellationToken cancellationToken) { - while (!cancellationToken.IsCancellationRequested) + var result = 0; + while (!cancellationToken.IsCancellationRequested + && (result = UnmanagedMethods.GetMessage(out var msg, IntPtr.Zero, 0, 0)) > 0) { - UnmanagedMethods.MSG msg; - UnmanagedMethods.GetMessage(out msg, IntPtr.Zero, 0, 0); UnmanagedMethods.TranslateMessage(ref msg); UnmanagedMethods.DispatchMessage(ref msg); } + if (result < 0) + { + Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.Win32Platform) + ?.Log(this, "Unmanaged error in message loop. Error Code: {0}", Marshal.GetLastWin32Error()); + } } public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action callback) From b31b6d68c9791085766550cdc29bcde12b1608a0 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 13 Nov 2020 16:17:03 +0100 Subject: [PATCH 030/164] Fixes ProcessMessage --- src/Windows/Avalonia.Win32/Win32Platform.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 209cb63603..1ed4d8dc9c 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -129,10 +129,18 @@ namespace Avalonia.Win32 public void ProcessMessage() { - UnmanagedMethods.MSG msg; - UnmanagedMethods.GetMessage(out msg, IntPtr.Zero, 0, 0); - UnmanagedMethods.TranslateMessage(ref msg); - UnmanagedMethods.DispatchMessage(ref msg); + + if (UnmanagedMethods.GetMessage(out var msg, IntPtr.Zero, 0, 0) > 0) + { + UnmanagedMethods.TranslateMessage(ref msg); + UnmanagedMethods.DispatchMessage(ref msg); + } + else + { + Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.Win32Platform) + ?.Log(this, "Unmanaged error in {0}. Error Code: {1}", nameof(ProcessMessage), Marshal.GetLastWin32Error()); + + } } public void RunLoop(CancellationToken cancellationToken) @@ -147,7 +155,7 @@ namespace Avalonia.Win32 if (result < 0) { Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.Win32Platform) - ?.Log(this, "Unmanaged error in message loop. Error Code: {0}", Marshal.GetLastWin32Error()); + ?.Log(this, "Unmanaged error in {0}. Error Code: {1}" ,nameof(RunLoop), Marshal.GetLastWin32Error()); } } From cfb81aedcaae7d501017a4224f18ffc8348ae4de Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Sat, 14 Nov 2020 00:44:44 +0100 Subject: [PATCH 031/164] fixes typo. --- src/Avalonia.Base/Logging/LogArea.cs | 2 +- src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs | 2 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs index ce330247f3..2ad220dddd 100644 --- a/src/Avalonia.Base/Logging/LogArea.cs +++ b/src/Avalonia.Base/Logging/LogArea.cs @@ -36,7 +36,7 @@ namespace Avalonia.Logging public const string Control = "Control"; /// - /// The log evevnt come from Win32Platform + /// The log event comes from Win32Platform. /// public const string Win32Platform = nameof(Win32Platform); } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 85938829f6..b164bcc611 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -931,7 +931,7 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll", EntryPoint = "MapVirtualKeyW")] public static extern uint MapVirtualKey(uint uCode, uint uMapType); - [DllImport("user32.dll", EntryPoint = "GetMessageW",SetLastError = true)] + [DllImport("user32.dll", EntryPoint = "GetMessageW", SetLastError = true)] public static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); [DllImport("user32.dll")] diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 1ed4d8dc9c..345c825a5d 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -155,7 +155,7 @@ namespace Avalonia.Win32 if (result < 0) { Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.Win32Platform) - ?.Log(this, "Unmanaged error in {0}. Error Code: {1}" ,nameof(RunLoop), Marshal.GetLastWin32Error()); + ?.Log(this, "Unmanaged error in {0}. Error Code: {1}", nameof(RunLoop), Marshal.GetLastWin32Error()); } } From 7ff860c8ef54c5ce38b2e6c672297e651c1142c2 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Sat, 14 Nov 2020 14:40:21 +0100 Subject: [PATCH 032/164] fix ProcessMessage issue --- src/Windows/Avalonia.Win32/Win32Platform.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 345c825a5d..e854ceae0b 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -130,7 +130,7 @@ namespace Avalonia.Win32 public void ProcessMessage() { - if (UnmanagedMethods.GetMessage(out var msg, IntPtr.Zero, 0, 0) > 0) + if (UnmanagedMethods.GetMessage(out var msg, IntPtr.Zero, 0, 0) > -1) { UnmanagedMethods.TranslateMessage(ref msg); UnmanagedMethods.DispatchMessage(ref msg); From 80a87d7aa31a8b177690207d3d043e90d68f093e Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 15 Nov 2020 17:32:43 +0200 Subject: [PATCH 033/164] add failing tests for CompiledBidning with Source set to StaticResource and x:Static --- .../CompiledBindingExtensionTests.cs | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 50dfa2c7b0..c19640c244 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Data.Converters; using Avalonia.Data.Core; using Avalonia.Markup.Data; +using Avalonia.Media; using Avalonia.UnitTests; using XamlX; using Xunit; @@ -536,7 +537,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } [Fact] - public void ResolvesSourceBindingLongForm() + public void Binds_To_Source() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -559,6 +560,49 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void Binds_To_Source_StaticResource() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + +"; + + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + Assert.Equal("foobar", textBlock.Text); + } + } + + [Fact] + public void Binds_To_Source_xStatic() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var contentControl = window.FindControl("contentControl"); + + Assert.Equal(Brushes.Red.Color, contentControl.Content); + } + } + [Fact] public void CompilesBindingWhenRequested() { From 06d81f6e23cde111166393f441558dc1354247df Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 15 Nov 2020 17:40:34 +0200 Subject: [PATCH 034/164] make CompiledBinding work with x:Static and StaticResource --- .../AvaloniaXamlIlBindingPathTransformer.cs | 33 ++++++++++++++++--- ...ransformSyntheticCompiledBindingMembers.cs | 4 +-- .../CompiledBindingExtension.cs | 10 ++++++ 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs index fc32084687..0d54e97bd9 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs @@ -15,14 +15,37 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { if (node is XamlAstConstructableObjectNode binding && binding.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)) { - IXamlType startType; - var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault(); - if (parentDataContextNode is null) + IXamlType startType = null; + var sourceProperty = binding.Children.OfType().FirstOrDefault(c => c.Property.Name == "Source"); + if ((sourceProperty?.Values.Count ?? 0) == 1) { - throw new XamlX.XamlParseException("Cannot parse a compiled binding without an explicit x:DataType directive to give a starting data type for bindings.", binding); + var sourceValue = sourceProperty.Values[0]; + switch (sourceValue) + { + case XamlAstTextNode textNode: + startType = textNode.Type?.GetClrType(); + break; + + case XamlMarkupExtensionNode extension: + startType = extension.Type?.GetClrType(); + break; + + case XamlStaticExtensionNode staticExtension: + startType = staticExtension.Type?.GetClrType(); + break; + } } - startType = parentDataContextNode.DataContextType; + if (startType == null) + { + var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault(); + if (parentDataContextNode is null) + { + throw new XamlX.XamlParseException("Cannot parse a compiled binding without an explicit x:DataType directive to give a starting data type for bindings.", binding); + } + + startType = parentDataContextNode.DataContextType; + } XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, binding, startType); } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs index 154c6a235c..6cb6e4e2d1 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs @@ -27,8 +27,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } else if (prop.Name == "Source") { - return new AvaloniaSyntheticCompiledBindingProperty(node, - SyntheticCompiledBindingPropertyName.Source); + //return new AvaloniaSyntheticCompiledBindingProperty(node, + // SyntheticCompiledBindingPropertyName.Source); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs index da39920eb3..17d2ea7ae9 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs @@ -32,6 +32,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions Mode = Mode, Priority = Priority, StringFormat = StringFormat, + Source = Source, DefaultAnchor = new WeakReference(GetDefaultAnchor(provider)) }; } @@ -52,6 +53,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) { + if (Source != null) + { + return CreateSourceObserver( + Source, + Path.BuildExpression(enableDataValidation)); + } + if (Path.RawSource != null) { return CreateSourceObserver( @@ -77,5 +85,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions [ConstructorArgument("path")] public CompiledBindingPath Path { get; set; } + + public object Source { get; set; } } } From f9de2a9c8236573df5ffe8dd0acc4417f3e43665 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 15 Nov 2020 17:42:05 +0200 Subject: [PATCH 035/164] add more tests for CompiledBinding Source with StaticResource --- .../CompiledBindingExtensionTests.cs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index c19640c244..de9ecc8bb0 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -583,6 +583,81 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void Binds_To_Source_StaticResource1() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + test + + +"; + + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + Assert.Equal("foobar", textBlock.Text); + } + } + + [Fact] + public void Binds_To_Source_StaticResource_In_ResourceDictionary() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + +"; + + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + Assert.Equal("foobar", textBlock.Text); + } + } + + [Fact] + public void Binds_To_Source_StaticResource_In_ResourceDictionary1() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + test + + + +"; + + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + Assert.Equal("foobar", textBlock.Text); + } + } + [Fact] public void Binds_To_Source_xStatic() { From 13c25983f70d2c606eaaa5d83f318413bf65773b Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 15 Nov 2020 17:45:04 +0200 Subject: [PATCH 036/164] try infer StaticResource type in CompiledBinding Soure --- .../AvaloniaXamlIlBindingPathTransformer.cs | 67 ++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs index 0d54e97bd9..4d8c940bbc 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using XamlX; using XamlX.Ast; using XamlX.Transform; using XamlX.TypeSystem; @@ -28,6 +26,71 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers case XamlMarkupExtensionNode extension: startType = extension.Type?.GetClrType(); + + //let's try to infer StaticResource type from parent resources in xaml + if (extension.Value.Type.GetClrType().FullName == "Avalonia.Markup.Xaml.MarkupExtensions.StaticResourceExtension" && + extension.Value is XamlAstConstructableObjectNode cn && + cn.Arguments.Count == 1 && cn.Arguments[0] is XamlAstTextNode keyNode) + { + bool matchProperty(IXamlAstNode node, IXamlType styledElementType, string propertyName) + { + return (node is XamlPropertyAssignmentNode p && + p.Property.DeclaringType == styledElementType && p.Property.Name == propertyName) + || + (node is XamlManipulationGroupNode m && m.Children.Count > 0 && + m.Children[0] is XamlPropertyAssignmentNode pm && + pm.Property.DeclaringType == styledElementType && pm.Property.Name == propertyName); + } + + string getResourceValue_xKey(XamlPropertyAssignmentNode node) + => node.Values.Count == 2 && node.Values[0] is XamlAstTextNode t ? t.Text : ""; + + IXamlType getResourceValue_Type(XamlPropertyAssignmentNode node, IXamlType xamlType) + => node.Values.Count == 2 ? node.Values[1].Type.GetClrType() : xamlType; + + IEnumerable getResourceValues(IXamlAstNode node) + { + if (node is XamlPropertyAssignmentNode propertyNode) + { + if (propertyNode.Values.Count == 1 && + propertyNode.Values[0] is XamlAstConstructableObjectNode obj && + obj.Type.GetClrType().FullName == "Avalonia.Controls.ResourceDictionary") + { + foreach (var r in obj.Children.SelectMany(c => getResourceValues(c))) + { + yield return r; + } + } + else + { + yield return propertyNode; + } + } + else if (node is XamlManipulationGroupNode m) + { + foreach (var r in m.Children.OfType()) + { + yield return r; + } + } + } + + string key = keyNode.Text; + + var styledElement = context.GetAvaloniaTypes().StyledElement; + var resource = context.ParentNodes() + .OfType() + .Where(o => styledElement.IsAssignableFrom(o.Type.GetClrType())) + .Select(o => o.Children.FirstOrDefault(p => matchProperty(p, styledElement, "Resources"))) + .Where(r => r != null) + .SelectMany(r => getResourceValues(r)) + .FirstOrDefault(r => getResourceValue_xKey(r) == key); + + if (resource != null) + { + startType = getResourceValue_Type(resource, startType); + } + } break; case XamlStaticExtensionNode staticExtension: From bc39305fd0a088a306e8c13358fa8fc426fe9fc0 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 15 Nov 2020 18:16:58 +0200 Subject: [PATCH 037/164] clean up --- .../AvaloniaXamlIlBindingPathParser.cs | 23 ++++++------------- ...ransformSyntheticCompiledBindingMembers.cs | 8 +------ 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs index 7944d8b569..24cded1d22 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs @@ -72,16 +72,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers v.Property is AvaloniaSyntheticCompiledBindingProperty prop && prop.Name == SyntheticCompiledBindingPropertyName.ElementName); - var sourceProperty = syntheticCompiledBindingProperties - .FirstOrDefault(v => - v.Property is AvaloniaSyntheticCompiledBindingProperty prop - && prop.Name == SyntheticCompiledBindingPropertyName.Source); - var relativeSourceProperty = syntheticCompiledBindingProperties .FirstOrDefault(v => v.Property is AvaloniaSyntheticCompiledBindingProperty prop && prop.Name == SyntheticCompiledBindingPropertyName.RelativeSource); + var sourceProperty = binding.Children.OfType() + .FirstOrDefault(v => + v.Property is XamlAstClrProperty prop + && prop.Name == "Source"); + if (elementNameProperty?.Values[0] is XamlAstTextNode elementName) { convertedNode = new BindingExpressionGrammar.NameNode { Name = elementName.Text }; @@ -91,14 +91,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers throw new XamlParseException($"Invalid ElementName '{elementNameProperty.Values[0]}'.", elementNameProperty.Values[0]); } - if (sourceProperty?.Values[0] != null) + if (sourceProperty != null && convertedNode != null) { - if (convertedNode != null) - { - throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding); - } - - convertedNode = new RawSourceBindingExpressionNode(sourceProperty?.Values[0]); + throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding); } if (GetRelativeSourceObjectFromAssignment( @@ -223,10 +218,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { binding.Children.Remove(elementNameProperty); } - if (sourceProperty != null) - { - binding.Children.Remove(sourceProperty); - } if (relativeSourceProperty != null) { binding.Children.Remove(relativeSourceProperty); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs index 6cb6e4e2d1..ba05347a52 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs @@ -25,11 +25,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers return new AvaloniaSyntheticCompiledBindingProperty(node, SyntheticCompiledBindingPropertyName.RelativeSource); } - else if (prop.Name == "Source") - { - //return new AvaloniaSyntheticCompiledBindingProperty(node, - // SyntheticCompiledBindingPropertyName.Source); - } } return node; @@ -39,8 +34,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers enum SyntheticCompiledBindingPropertyName { ElementName, - RelativeSource, - Source + RelativeSource } class AvaloniaSyntheticCompiledBindingProperty : XamlAstNode, IXamlAstPropertyReference From a806d4bd4734c04014f5fe9d3d0e7e92ca802588 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Tue, 24 Nov 2020 18:01:34 +0800 Subject: [PATCH 038/164] Add keyboard navigation to slider --- src/Avalonia.Controls/Slider.cs | 74 +++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index fe4b24099f..7690f474f3 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -188,6 +188,80 @@ namespace Avalonia.Controls _pointerMovedDispose = this.AddDisposableHandler(PointerMovedEvent, TrackMoved, RoutingStrategies.Tunnel); } + private void TrackOnKeyDown(object sender, KeyEventArgs e) + { + if (e.KeyModifiers != KeyModifiers.None) return; + + switch (e.Key) + { + case Key.Left: + MoveToNextTick(-SmallChange); + break; + + case Key.Right: + MoveToNextTick(SmallChange); + break; + + case Key.PageUp: + MoveToNextTick(-LargeChange); + break; + + case Key.PageDown: + MoveToNextTick(LargeChange); + break; + + case Key.Home: + Value = Minimum; + break; + + case Key.End: + Value = Maximum; + break; + } + } + + private void MoveToNextTick(double direction) + { + if (direction == 0.0) return; + + var value = Value; + + // Find the next value by snapping + var next = SnapToTick(Math.Max(Minimum, Math.Min(Maximum, value + direction))); + + var greaterThan = direction > 0; //search for the next tick greater than value? + + // If the snapping brought us back to value, find the next tick point + if (Math.Abs(next - value) < Tolerance + && !(greaterThan && Math.Abs(value - Maximum) < Tolerance) // Stop if searching up if already at Max + && !(!greaterThan && Math.Abs(value - Minimum) < Tolerance)) // Stop if searching down if already at Min + { + var ticks = Ticks; + + // If ticks collection is available, use it. + // Note that ticks may be unsorted. + if (ticks != null && ticks.Count > 0) + { + foreach (var tick in ticks) + { + // Find the smallest tick greater than value or the largest tick less than value + if (greaterThan && MathUtilities.GreaterThan(tick, value) && + (MathUtilities.LessThan(tick, next) || Math.Abs(next - value) < Tolerance) + || !greaterThan && MathUtilities.LessThan(tick, value) && + (MathUtilities.GreaterThan(tick, next) || Math.Abs(next - value) < Tolerance)) + { + next = tick; + } + } + } + else if (MathUtilities.GreaterThan(TickFrequency, 0.0)) + { + // Find the current tick we are at + var tickNumber = Math.Round((value - Minimum) / TickFrequency); + + _pointerMovedDispose = this.AddDisposableHandler(PointerMovedEvent, TrackMoved, RoutingStrategies.Tunnel); + } + private void TrackMoved(object sender, PointerEventArgs e) { if (_isDragging) From 52c14f65b6562dc99eec0831a5bd4b647c65ebe2 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Tue, 24 Nov 2020 18:04:04 +0800 Subject: [PATCH 039/164] Add keyboard navigation to slider --- src/Avalonia.Controls/Slider.cs | 36 ++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 7690f474f3..c55a391928 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -11,7 +11,6 @@ using Avalonia.Utilities; namespace Avalonia.Controls { - /// /// Enum which describes how to position the ticks in a . /// @@ -84,6 +83,8 @@ namespace Avalonia.Controls private IDisposable _increaseButtonSubscription; private IDisposable _increaseButtonReleaseDispose; private IDisposable _pointerMovedDispose; + + private const double Tolerance = 0.0001; /// /// Initializes static members of the class. @@ -95,7 +96,7 @@ namespace Avalonia.Controls Thumb.DragStartedEvent.AddClassHandler((x, e) => x.OnThumbDragStarted(e), RoutingStrategies.Bubble); Thumb.DragCompletedEvent.AddClassHandler((x, e) => x.OnThumbDragCompleted(e), RoutingStrategies.Bubble); - + ValueProperty.OverrideMetadata(new DirectPropertyMetadata(enableDataValidation: true)); } @@ -157,7 +158,7 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - + _decreaseButtonPressDispose?.Dispose(); _decreaseButtonReleaseDispose?.Dispose(); _increaseButtonSubscription?.Dispose(); @@ -168,9 +169,11 @@ namespace Avalonia.Controls _track = e.NameScope.Find("PART_Track"); _increaseButton = e.NameScope.Find /// The type. /// The metadata. - protected void OverrideMetadata(Type type, PropertyMetadata metadata) + protected void OverrideMetadata(Type type, AvaloniaPropertyMetadata metadata) { Contract.Requires(type != null); Contract.Requires(metadata != null); @@ -542,14 +542,14 @@ namespace Avalonia protected abstract IObservable GetChanged(); - private PropertyMetadata GetMetadataWithOverrides(Type type) + private AvaloniaPropertyMetadata GetMetadataWithOverrides(Type type) { if (type is null) { throw new ArgumentNullException(nameof(type)); } - if (_metadataCache.TryGetValue(type, out PropertyMetadata result)) + if (_metadataCache.TryGetValue(type, out AvaloniaPropertyMetadata result)) { return result; } diff --git a/src/Avalonia.Base/PropertyMetadata.cs b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs similarity index 85% rename from src/Avalonia.Base/PropertyMetadata.cs rename to src/Avalonia.Base/AvaloniaPropertyMetadata.cs index 806051e1d1..2963567b14 100644 --- a/src/Avalonia.Base/PropertyMetadata.cs +++ b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs @@ -5,15 +5,15 @@ namespace Avalonia /// /// Base class for avalonia property metadata. /// - public class PropertyMetadata + public class AvaloniaPropertyMetadata { private BindingMode _defaultBindingMode; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The default binding mode. - public PropertyMetadata( + public AvaloniaPropertyMetadata( BindingMode defaultBindingMode = BindingMode.Default) { _defaultBindingMode = defaultBindingMode; @@ -37,7 +37,7 @@ namespace Avalonia /// The base metadata to merge. /// The property to which the metadata is being applied. public virtual void Merge( - PropertyMetadata baseMetadata, + AvaloniaPropertyMetadata baseMetadata, AvaloniaProperty property) { if (_defaultBindingMode == BindingMode.Default) diff --git a/src/Avalonia.Base/AvaloniaProperty`1.cs b/src/Avalonia.Base/AvaloniaProperty`1.cs index d5549e979b..247ea0a25a 100644 --- a/src/Avalonia.Base/AvaloniaProperty`1.cs +++ b/src/Avalonia.Base/AvaloniaProperty`1.cs @@ -23,7 +23,7 @@ namespace Avalonia protected AvaloniaProperty( string name, Type ownerType, - PropertyMetadata metadata, + AvaloniaPropertyMetadata metadata, Action notifying = null) : base(name, typeof(TValue), ownerType, metadata, notifying) { @@ -40,7 +40,7 @@ namespace Avalonia protected AvaloniaProperty( AvaloniaProperty source, Type ownerType, - PropertyMetadata metadata) + AvaloniaPropertyMetadata metadata) : this(source as AvaloniaProperty ?? throw new InvalidOperationException(), ownerType, metadata) { } @@ -54,7 +54,7 @@ namespace Avalonia protected AvaloniaProperty( AvaloniaProperty source, Type ownerType, - PropertyMetadata metadata) + AvaloniaPropertyMetadata metadata) : base(source, ownerType, metadata) { _changed = source._changed; diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index a2f113adb7..e6cc1edfdf 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -26,7 +26,7 @@ namespace Avalonia protected DirectPropertyBase( string name, Type ownerType, - PropertyMetadata metadata) + AvaloniaPropertyMetadata metadata) : base(name, ownerType, metadata) { } @@ -41,7 +41,7 @@ namespace Avalonia protected DirectPropertyBase( AvaloniaProperty source, Type ownerType, - PropertyMetadata metadata) + AvaloniaPropertyMetadata metadata) : this(source as DirectPropertyBase ?? throw new InvalidOperationException(), ownerType, metadata) { } @@ -55,7 +55,7 @@ namespace Avalonia protected DirectPropertyBase( DirectPropertyBase source, Type ownerType, - PropertyMetadata metadata) + AvaloniaPropertyMetadata metadata) : base(source, ownerType, metadata) { } diff --git a/src/Avalonia.Base/DirectPropertyMetadata`1.cs b/src/Avalonia.Base/DirectPropertyMetadata`1.cs index 59a60507dc..205967984d 100644 --- a/src/Avalonia.Base/DirectPropertyMetadata`1.cs +++ b/src/Avalonia.Base/DirectPropertyMetadata`1.cs @@ -5,7 +5,7 @@ namespace Avalonia /// /// Metadata for direct avalonia properties. /// - public class DirectPropertyMetadata : PropertyMetadata, IDirectPropertyMetadata + public class DirectPropertyMetadata : AvaloniaPropertyMetadata, IDirectPropertyMetadata { /// /// Initializes a new instance of the class. @@ -47,7 +47,7 @@ namespace Avalonia object IDirectPropertyMetadata.UnsetValue => UnsetValue; /// - public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty property) + public override void Merge(AvaloniaPropertyMetadata baseMetadata, AvaloniaProperty property) { base.Merge(baseMetadata, property); diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index cf0a0c34ec..ec7bff4dfb 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -6,7 +6,7 @@ namespace Avalonia /// /// Metadata for styled avalonia properties. /// - public class StyledPropertyMetadata : PropertyMetadata, IStyledPropertyMetadata + public class StyledPropertyMetadata : AvaloniaPropertyMetadata, IStyledPropertyMetadata { private Optional _defaultValue; @@ -39,7 +39,7 @@ namespace Avalonia object IStyledPropertyMetadata.DefaultValue => DefaultValue; /// - public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty property) + public override void Merge(AvaloniaPropertyMetadata baseMetadata, AvaloniaProperty property) { base.Merge(baseMetadata, property); diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 8e5d8b7be2..1adb6988a2 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -27,7 +27,7 @@ namespace Avalonia.Base.UnitTests [Fact] public void GetMetadata_Returns_Supplied_Value() { - var metadata = new PropertyMetadata(); + var metadata = new AvaloniaPropertyMetadata(); var target = new TestProperty("test", typeof(Class1), metadata); Assert.Same(metadata, target.GetMetadata()); @@ -36,7 +36,7 @@ namespace Avalonia.Base.UnitTests [Fact] public void GetMetadata_Returns_Supplied_Value_For_Derived_Class() { - var metadata = new PropertyMetadata(); + var metadata = new AvaloniaPropertyMetadata(); var target = new TestProperty("test", typeof(Class1), metadata); Assert.Same(metadata, target.GetMetadata()); @@ -45,7 +45,7 @@ namespace Avalonia.Base.UnitTests [Fact] public void GetMetadata_Returns_Supplied_Value_For_Unrelated_Class() { - var metadata = new PropertyMetadata(); + var metadata = new AvaloniaPropertyMetadata(); var target = new TestProperty("test", typeof(Class3), metadata); Assert.Same(metadata, target.GetMetadata()); @@ -54,8 +54,8 @@ namespace Avalonia.Base.UnitTests [Fact] public void GetMetadata_Returns_Overridden_Value() { - var metadata = new PropertyMetadata(); - var overridden = new PropertyMetadata(); + var metadata = new AvaloniaPropertyMetadata(); + var overridden = new AvaloniaPropertyMetadata(); var target = new TestProperty("test", typeof(Class1), metadata); target.OverrideMetadata(overridden); @@ -66,9 +66,9 @@ namespace Avalonia.Base.UnitTests [Fact] public void OverrideMetadata_Should_Merge_Values() { - var metadata = new PropertyMetadata(BindingMode.TwoWay); + var metadata = new AvaloniaPropertyMetadata(BindingMode.TwoWay); var notify = (Action)((a, b) => { }); - var overridden = new PropertyMetadata(); + var overridden = new AvaloniaPropertyMetadata(); var target = new TestProperty("test", typeof(Class1), metadata); target.OverrideMetadata(overridden); @@ -129,19 +129,19 @@ namespace Avalonia.Base.UnitTests [Fact] public void PropertyMetadata_BindingMode_Default_Returns_OneWay() { - var data = new PropertyMetadata(defaultBindingMode: BindingMode.Default); + var data = new AvaloniaPropertyMetadata(defaultBindingMode: BindingMode.Default); Assert.Equal(BindingMode.OneWay, data.DefaultBindingMode); } private class TestProperty : AvaloniaProperty { - public TestProperty(string name, Type ownerType, PropertyMetadata metadata = null) - : base(name, ownerType, metadata ?? new PropertyMetadata()) + public TestProperty(string name, Type ownerType, AvaloniaPropertyMetadata metadata = null) + : base(name, ownerType, metadata ?? new AvaloniaPropertyMetadata()) { } - public void OverrideMetadata(PropertyMetadata metadata) + public void OverrideMetadata(AvaloniaPropertyMetadata metadata) { OverrideMetadata(typeof(T), metadata); } From 41db07a6b69dd6e4a49e08956c7761bfa481af43 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 10 Dec 2020 12:04:05 +0000 Subject: [PATCH 043/164] baseline for breaking change. --- src/Avalonia.Base/ApiCompatBaseline.txt | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/ApiCompatBaseline.txt b/src/Avalonia.Base/ApiCompatBaseline.txt index d17f25ec49..09baff5af3 100644 --- a/src/Avalonia.Base/ApiCompatBaseline.txt +++ b/src/Avalonia.Base/ApiCompatBaseline.txt @@ -1,4 +1,20 @@ Compat issues with assembly Avalonia.Base: +MembersMustExist : Member 'protected void Avalonia.AvaloniaProperty..ctor(Avalonia.AvaloniaProperty, System.Type, Avalonia.PropertyMetadata)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.AvaloniaProperty..ctor(System.String, System.Type, System.Type, Avalonia.PropertyMetadata, System.Action)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.PropertyMetadata Avalonia.AvaloniaProperty.GetMetadata(System.Type)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.PropertyMetadata Avalonia.AvaloniaProperty.GetMetadata()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.AvaloniaProperty.OverrideMetadata(System.Type, Avalonia.PropertyMetadata)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.AvaloniaProperty..ctor(Avalonia.AvaloniaProperty, System.Type, Avalonia.PropertyMetadata)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.AvaloniaProperty..ctor(Avalonia.AvaloniaProperty, System.Type, Avalonia.PropertyMetadata)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.AvaloniaProperty..ctor(System.String, System.Type, Avalonia.PropertyMetadata, System.Action)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.DirectPropertyBase..ctor(Avalonia.AvaloniaProperty, System.Type, Avalonia.PropertyMetadata)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.DirectPropertyBase..ctor(Avalonia.DirectPropertyBase, System.Type, Avalonia.PropertyMetadata)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.DirectPropertyBase..ctor(System.String, System.Type, Avalonia.PropertyMetadata)' does not exist in the implementation but it does exist in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.DirectPropertyMetadata' does not inherit from base type 'Avalonia.PropertyMetadata' in the implementation but it does in the contract. +MembersMustExist : Member 'public void Avalonia.DirectPropertyMetadata.Merge(Avalonia.PropertyMetadata, Avalonia.AvaloniaProperty)' does not exist in the implementation but it does exist in the contract. +TypesMustExist : Type 'Avalonia.PropertyMetadata' does not exist in the implementation but it does exist in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.StyledPropertyMetadata' does not inherit from base type 'Avalonia.PropertyMetadata' in the implementation but it does in the contract. +MembersMustExist : Member 'public void Avalonia.StyledPropertyMetadata.Merge(Avalonia.PropertyMetadata, Avalonia.AvaloniaProperty)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Threading.AvaloniaSynchronizationContext..ctor(Avalonia.Threading.AvaloniaSynchronizationContext.INonPumpingPlatformWaitProvider)' does not exist in the implementation but it does exist in the contract. TypesMustExist : Type 'Avalonia.Threading.AvaloniaSynchronizationContext.INonPumpingPlatformWaitProvider' does not exist in the implementation but it does exist in the contract. -Total Issues: 2 +Total Issues: 18 From 5d49a5a5bc7f261bc643093767fcff9b2f69439b Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 10 Dec 2020 14:02:14 +0200 Subject: [PATCH 044/164] fix datagrid virtualization in some situations --- .../Primitives/DataGridRowsPresenter.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs index 6ad58bf250..76e5427fa4 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs @@ -22,6 +22,22 @@ namespace Avalonia.Controls.Primitives } private double _measureHeightOffset = 0; + private double _effectiveViewPortHeight = 0; + + public DataGridRowsPresenter() + { + EffectiveViewportChanged += OnEffectiveViewportChanged; + } + + private void OnEffectiveViewportChanged(object sender, Layout.EffectiveViewportChangedEventArgs e) + { + if (_effectiveViewPortHeight != e.EffectiveViewport.Height) + { + _effectiveViewPortHeight = e.EffectiveViewport.Height; + InvalidateMeasure(); + } + } + private double CalculateEstimatedAvailableHeight(Size availableSize) { if(!Double.IsPositiveInfinity(availableSize.Height)) @@ -108,6 +124,11 @@ namespace Avalonia.Controls.Primitives /// protected override Size MeasureOverride(Size availableSize) { + if (double.IsInfinity(availableSize.Height)) + { + availableSize = availableSize.WithHeight(_effectiveViewPortHeight); + } + if (availableSize.Height == 0 || OwningGrid == null) { return base.MeasureOverride(availableSize); From a98ab33373d67d1b91c23a3e4320148d5ca62b26 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 10 Dec 2020 20:07:16 +0800 Subject: [PATCH 045/164] use disposable handler instead. --- src/Avalonia.Controls/Slider.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index c55a391928..ed7a214ca2 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -83,7 +83,8 @@ namespace Avalonia.Controls private IDisposable _increaseButtonSubscription; private IDisposable _increaseButtonReleaseDispose; private IDisposable _pointerMovedDispose; - + private IDisposable _trackOnKeyDownDispose; + private const double Tolerance = 0.0001; /// @@ -164,16 +165,16 @@ namespace Avalonia.Controls _increaseButtonSubscription?.Dispose(); _increaseButtonReleaseDispose?.Dispose(); _pointerMovedDispose?.Dispose(); - + _trackOnKeyDownDispose?.Dispose(); + _decreaseButton = e.NameScope.Find