diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs
index 7a876ac862..a4d15bab8d 100644
--- a/src/Avalonia.Controls/Button.cs
+++ b/src/Avalonia.Controls/Button.cs
@@ -32,7 +32,7 @@ namespace Avalonia.Controls
/// A standard button control.
///
[PseudoClasses(pcFlyoutOpen, pcPressed)]
- public class Button : ContentControl, ICommandSource
+ public class Button : ContentControl, ICommandSource, IClickableControl
{
protected const string pcPressed = ":pressed";
protected const string pcFlyoutOpen = ":flyout-open";
@@ -633,6 +633,8 @@ namespace Avalonia.Controls
void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e);
+ void IClickableControl.RaiseClick() => OnClick();
+
///
/// Event handler for when the button's flyout is opened.
///
diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs
index d83b996aa0..bde1da509b 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 cddd621b8e..c03c6000d8 100644
--- a/src/Avalonia.Controls/MenuItem.cs
+++ b/src/Avalonia.Controls/MenuItem.cs
@@ -22,7 +22,7 @@ namespace Avalonia.Controls
///
[TemplatePart("PART_Popup", typeof(Popup))]
[PseudoClasses(":separator", ":icon", ":open", ":pressed", ":selected")]
- public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable, ICommandSource
+ public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable, ICommandSource, IClickableControl
{
///
/// Defines the property.
@@ -705,6 +705,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")]
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