From d62381a0b218e063fcfb7b725130e5fead597428 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 31 Jan 2022 17:39:09 +0100 Subject: [PATCH 1/3] feat: Allowed Hotkey without using Command --- src/Avalonia.Controls/Button.cs | 4 +- src/Avalonia.Controls/HotkeyManager.cs | 67 +++++++++++++++---- src/Avalonia.Controls/MenuItem.cs | 10 ++- src/Avalonia.Input/IClickableControl.cs | 18 +++++ src/Avalonia.Input/Properties/AssemblyInfo.cs | 4 ++ 5 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 src/Avalonia.Input/IClickableControl.cs diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 899521536f..42569111d3 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -32,7 +32,7 @@ namespace Avalonia.Controls /// A button control. /// [PseudoClasses(":pressed")] - public class Button : ContentControl, ICommandSource + public class Button : ContentControl, ICommandSource, IClickableControl { /// /// Defines the property. @@ -566,5 +566,7 @@ namespace Avalonia.Controls } void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); + + void IClickableControl.RaiseClick() => OnClick(); } } diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index d83b996aa0..dcde70a2ff 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -12,21 +12,61 @@ namespace Avalonia.Controls class HotkeyCommandWrapper : ICommand { - public HotkeyCommandWrapper(ICommandSource? control) + readonly WeakReference reference; + + public HotkeyCommandWrapper(IControl control) { - CommandSource = control; + reference = new WeakReference(control); } - public readonly ICommandSource? CommandSource; + public ICommand GetCommand() + { + if (reference.Target is { } target) + { + if (target is ICommandSource commandSource && commandSource.Command is { } command) + { + return command; + } + else if (target is IClickableControl { }) + { + return this; + } + } + return null; + } - private ICommand? GetCommand() => CommandSource?.Command; + public bool CanExecute(object parameter) + { + if (reference.Target is { } target) + { + if (target is ICommandSource commandSource && commandSource.Command is { } command) + { + return commandSource.IsEffectivelyEnabled + && command.CanExecute(commandSource.CommandParameter) == true; + } + else if (target is IClickableControl clickable) + { + return clickable.IsEffectivelyEnabled; + } + } + return false; + } - public bool CanExecute(object? parameter) => - CommandSource?.Command?.CanExecute(CommandSource.CommandParameter) == true - && CommandSource.IsEffectivelyEnabled; + public void Execute(object parameter) + { + if (reference.Target is { } target) + { + if (target is ICommandSource commandSource && commandSource.Command is { } command) + { + command.Execute(commandSource.CommandParameter); + } + else if (target is IClickableControl { IsEffectivelyEnabled: true } clickable) + { + clickable.RaiseClick(); + } + } + } - public void Execute(object? parameter) => - GetCommand()?.Execute(CommandSource?.CommandParameter); #pragma warning disable 67 // Event not used public event EventHandler? CanExecuteChanged; @@ -47,7 +87,7 @@ namespace Avalonia.Controls public Manager(IControl control) { _control = control; - _wrapper = new HotkeyCommandWrapper(_control as ICommandSource); + _wrapper = new HotkeyCommandWrapper(_control); } public void Init() @@ -104,13 +144,14 @@ namespace Avalonia.Controls { HotKeyProperty.Changed.Subscribe(args => { - if (args.NewValue.Value is null) return; + if (args.NewValue.Value is null) + return; var control = args.Sender as IControl; - if (control is not ICommandSource) + if (control is not IClickableControl) { Logging.Logger.TryGet(Logging.LogEventLevel.Warning, Logging.LogArea.Control)?.Log(control, - $"The element {args.Sender.GetType().Name} does not implement ICommandSource and does not support binding a HotKey ({args.NewValue})."); + $"The element {args.Sender.GetType().Name} does not implement IClickableControl and does not support binding a HotKey ({args.NewValue})."); return; } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 34215e9713..d40693e721 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -21,7 +21,7 @@ namespace Avalonia.Controls /// A menu item control. /// [PseudoClasses(":separator", ":icon", ":open", ":pressed", ":selected")] - public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable, ICommandSource + public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable, ICommandSource, IClickableControl { /// /// Defines the property. @@ -704,6 +704,14 @@ namespace Avalonia.Controls void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); + void IClickableControl.RaiseClick() + { + if (IsEffectivelyEnabled) + { + RaiseEvent(new RoutedEventArgs(ClickEvent)); + } + } + /// /// A dependency resolver which returns a . /// diff --git a/src/Avalonia.Input/IClickableControl.cs b/src/Avalonia.Input/IClickableControl.cs new file mode 100644 index 0000000000..d6d887520b --- /dev/null +++ b/src/Avalonia.Input/IClickableControl.cs @@ -0,0 +1,18 @@ +using System; +using Avalonia.Interactivity; + +namespace Avalonia.Input +{ + /// + /// + /// + internal interface IClickableControl + { + event EventHandler Click; + void RaiseClick(); + /// + /// Gets a value indicating whether this control and all its parents are enabled. + /// + bool IsEffectivelyEnabled { get; } + } +} diff --git a/src/Avalonia.Input/Properties/AssemblyInfo.cs b/src/Avalonia.Input/Properties/AssemblyInfo.cs index 6a68bf60d1..a7126d6ba9 100644 --- a/src/Avalonia.Input/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Input/Properties/AssemblyInfo.cs @@ -1,6 +1,10 @@ using System.Reflection; +using System.Runtime.CompilerServices; using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.TextInput")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.GestureRecognizers")] + +[assembly: InternalsVisibleTo("Avalonia.Controls, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] From d3de4825542b7eef232a85bad4e693ddf5c6e974 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 31 Jan 2022 17:40:10 +0100 Subject: [PATCH 2/3] test: Added test for check Hotkey without command --- .../Utils/HotKeyManagerTests.cs | 149 +++++++++++++++++- 1 file changed, 144 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs index b0eb694944..8d9a4aa599 100644 --- a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs @@ -170,7 +170,7 @@ namespace Avalonia.Controls.UnitTests.Utils } [Theory] - [MemberData(nameof(ElementsFactory))] + [MemberData(nameof(ElementsFactory), parameters: true)] public void HotKeyManager_Should_Use_CommandParameter(string factoryName, Factory factory) { using (AvaloniaLocator.EnterScope()) @@ -215,7 +215,7 @@ namespace Avalonia.Controls.UnitTests.Utils [Theory] - [MemberData(nameof(ElementsFactory))] + [MemberData(nameof(ElementsFactory), parameters: true)] public void HotKeyManager_Should_Do_Not_Executed_When_IsEnabled_False(string factoryName, Factory factory) { using (AvaloniaLocator.EnterScope()) @@ -256,11 +256,127 @@ namespace Avalonia.Controls.UnitTests.Utils } } - public static TheoryData ElementsFactory => + [Theory] + [MemberData(nameof(ElementsFactory), parameters:false)] + public void HotKeyManager_Should_Invoke_Event_Click_When_Command_Is_Null(string factoryName, Factory factory) + { + using (AvaloniaLocator.EnterScope()) + { + var styler = new Mock(); + var target = new KeyboardDevice(); + var clickExecutedCount = 0; + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(new WindowingPlatformMock()) + .Bind().ToConstant(styler.Object); + + var gesture = new KeyGesture(Key.A, KeyModifiers.Control); + + void Clickable_Click(object sender, Interactivity.RoutedEventArgs e) + { + clickExecutedCount++; + } + + var root = new Window(); + var element = factory(0, default, root) as InputElement; + if (element is IClickableControl clickable) + { + clickable.Click += Clickable_Click; + } + + 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)); + + element.IsEnabled = false; + + target.ProcessRawEvent(new RawKeyEventArgs(target, + 0, + root, + RawKeyEventType.KeyDown, + Key.A, + RawInputModifiers.Control)); + + + Assert.True(clickExecutedCount == 1, $"{factoryName} Execution raised when IsEnabled is false."); + } + } + + [Theory] + [MemberData(nameof(ElementsFactory), parameters: true)] + public void HotKeyManager_Should_Not_Invoke_Event_Click_When_Command_Is_Not_Null(string factoryName, Factory factory) + { + using (AvaloniaLocator.EnterScope()) + { + var styler = new Mock(); + var target = new KeyboardDevice(); + var clickExecutedCount = 0; + var commandExecutedCount = 0; + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(new WindowingPlatformMock()) + .Bind().ToConstant(styler.Object); + + var gesture = new KeyGesture(Key.A, KeyModifiers.Control); + + void DoExecute(object parameter) + { + commandExecutedCount++; + } + + void Clickable_Click(object sender, Interactivity.RoutedEventArgs e) + { + clickExecutedCount++; + } + + var root = new Window(); + var element = factory(0, DoExecute, root) as InputElement; + if (element is IClickableControl clickable) + { + clickable.Click += Clickable_Click; + } + + 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)); + + element.IsEnabled = false; + + target.ProcessRawEvent(new RawKeyEventArgs(target, + 0, + root, + RawKeyEventType.KeyDown, + Key.A, + RawInputModifiers.Control)); + + Assert.True(commandExecutedCount == 1, $"{factoryName} Execution raised when IsEnabled is false."); + Assert.True(clickExecutedCount == 0, $"{factoryName} Execution raised event Click."); + } + } + + + public static TheoryData ElementsFactory(bool withCommand) => + new TheoryData() { - {nameof(Button), MakeButton}, - {nameof(MenuItem),MakeMenu}, + {nameof(Button), withCommand ? MakeButton : MakeButtonWithoutCommand}, + {nameof(MenuItem),withCommand ? MakeMenu : MakeMenuWithoutCommand}, }; private static AvaloniaObject MakeMenu(int expectedParameter, Action action, Window root) @@ -290,6 +406,29 @@ namespace Avalonia.Controls.UnitTests.Utils return button; } + private static AvaloniaObject MakeMenuWithoutCommand(int expectedParameter, Action action, Window root) + { + var menuitem = new MenuItem() + { + }; + var rootMenu = new Menu(); + + rootMenu.Items = new[] { menuitem }; + + root.Content = rootMenu; + return menuitem; + } + + private static AvaloniaObject MakeButtonWithoutCommand(int expectedParameter, Action action, Window root) + { + var button = new Button() + { + }; + + root.Content = button; + return button; + } + private FuncControlTemplate CreateWindowTemplate() { return new FuncControlTemplate((parent, scope) => From 6a54fc7e1be2cb2ba8ebcc01341c36af88d4e338 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 23 Mar 2022 18:23:38 +0100 Subject: [PATCH 3/3] fix(HotKeyManager): null annotation after rebase --- src/Avalonia.Controls/HotkeyManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index dcde70a2ff..bde1da509b 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -19,7 +19,7 @@ namespace Avalonia.Controls reference = new WeakReference(control); } - public ICommand GetCommand() + public ICommand? GetCommand() { if (reference.Target is { } target) { @@ -35,7 +35,7 @@ namespace Avalonia.Controls return null; } - public bool CanExecute(object parameter) + public bool CanExecute(object? parameter) { if (reference.Target is { } target) { @@ -52,7 +52,7 @@ namespace Avalonia.Controls return false; } - public void Execute(object parameter) + public void Execute(object? parameter) { if (reference.Target is { } target) {