Browse Source

Merge pull request #7500 from workgroupengineering/features/Controls/HotkeyManager_Without_Command

Allowed Hotkey without using Command
pull/7936/head
Max Katz 5 years ago
committed by GitHub
parent
commit
2d03f508ae
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      src/Avalonia.Controls/Button.cs
  2. 67
      src/Avalonia.Controls/HotkeyManager.cs
  3. 10
      src/Avalonia.Controls/MenuItem.cs
  4. 18
      src/Avalonia.Input/IClickableControl.cs
  5. 4
      src/Avalonia.Input/Properties/AssemblyInfo.cs
  6. 149
      tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs

4
src/Avalonia.Controls/Button.cs

@ -32,7 +32,7 @@ namespace Avalonia.Controls
/// A standard button control.
/// </summary>
[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();
/// <summary>
/// Event handler for when the button's flyout is opened.
/// </summary>

67
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;
}

10
src/Avalonia.Controls/MenuItem.cs

@ -22,7 +22,7 @@ namespace Avalonia.Controls
/// </summary>
[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
{
/// <summary>
/// Defines the <see cref="Command"/> 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));
}
}
/// <summary>
/// A dependency resolver which returns a <see cref="MenuItemAccessKeyHandler"/>.
/// </summary>

18
src/Avalonia.Input/IClickableControl.cs

@ -0,0 +1,18 @@
using System;
using Avalonia.Interactivity;
namespace Avalonia.Input
{
/// <summary>
///
/// </summary>
internal interface IClickableControl
{
event EventHandler<RoutedEventArgs> Click;
void RaiseClick();
/// <summary>
/// Gets a value indicating whether this control and all its parents are enabled.
/// </summary>
bool IsEffectivelyEnabled { get; }
}
}

4
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")]

149
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<string, Factory> 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<Styler>();
var target = new KeyboardDevice();
var clickExecutedCount = 0;
AvaloniaLocator.CurrentMutable
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformMock())
.Bind<IStyler>().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<Styler>();
var target = new KeyboardDevice();
var clickExecutedCount = 0;
var commandExecutedCount = 0;
AvaloniaLocator.CurrentMutable
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformMock())
.Bind<IStyler>().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<string, Factory> ElementsFactory(bool withCommand) =>
new TheoryData<string, Factory>()
{
{nameof(Button), MakeButton},
{nameof(MenuItem),MakeMenu},
{nameof(Button), withCommand ? MakeButton : MakeButtonWithoutCommand},
{nameof(MenuItem),withCommand ? MakeMenu : MakeMenuWithoutCommand},
};
private static AvaloniaObject MakeMenu(int expectedParameter, Action<object> action, Window root)
@ -290,6 +406,29 @@ namespace Avalonia.Controls.UnitTests.Utils
return button;
}
private static AvaloniaObject MakeMenuWithoutCommand(int expectedParameter, Action<object> 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<object> action, Window root)
{
var button = new Button()
{
};
root.Content = button;
return button;
}
private FuncControlTemplate CreateWindowTemplate()
{
return new FuncControlTemplate<Window>((parent, scope) =>

Loading…
Cancel
Save