зеркало из https://github.com/AvaloniaUI/Avalonia.git
Fixes and improves several access key (accelerator) related issues (#17292)
* fix accelerator behavior for menu items and labels * add elements with matching accelerator to test cycling in sub menus * Add AccessKeyHandler tests for accelerators with more than one match * Implement accelerator behavior based on WPF handling * Remove commented code * Remove OnAccessKey override => handled by DefaultMenuInteractionHandler * remove obsolete test * handle OnAccessKeyPressed for selected tab item * fix unit tests * use AccessKeyEvent instead of AccessKeyPressedEvent in unit tests * navigate menu with and without ALT key * Revert formatting changes in Tests * Fix AccessKeyHandler comments * move private types to bottom * Remove lock statements, optimize removal of AccessKeyRegistrations * remove call to Dispatcher.UIThread.Post * simplifiy AccessKeyHandler.SortByHierarchy * remove unnecessary method AccessKeyHandler.GetTargetsForSender * regenerate API suppression file * revert unneeded changes in MenuPage.axaml * correct formatting changes * do not sort by hierarchy if too few targets * make AccessKeyEventArgs internal * make AccessKeyPressedEventArgs internal --------- Co-authored-by: Hans Docsek <hans.docsek@gmail.com>
This commit is contained in:
Родитель
57b4be4b73
Коммит
b46126714b
|
@ -91,4 +91,10 @@
|
|||
<Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
|
||||
<Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0012</DiagnosticId>
|
||||
<Target>M:Avalonia.Controls.Button.OnAccessKey(Avalonia.Interactivity.RoutedEventArgs)</Target>
|
||||
<Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
|
||||
<Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
|
||||
</Suppression>
|
||||
</Suppressions>
|
|
@ -18,6 +18,9 @@
|
|||
<TabItem Header="Composition">
|
||||
<pages:CompositionPage />
|
||||
</TabItem>
|
||||
<TabItem Header="Accelerator">
|
||||
<pages:AcceleratorPage />
|
||||
</TabItem>
|
||||
<TabItem Header="Acrylic">
|
||||
<pages:AcrylicPage />
|
||||
</TabItem>
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="ControlCatalog.Pages.AcceleratorPage">
|
||||
<StackPanel Orientation="Vertical" Spacing="4">
|
||||
|
||||
<WrapPanel HorizontalAlignment="Left">
|
||||
<StackPanel>
|
||||
<Menu>
|
||||
<MenuItem Header="_First">
|
||||
<MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A" />
|
||||
<MenuItem Header="_Disabled Menu Item" IsEnabled="False" InputGesture="Ctrl+D" />
|
||||
<Separator />
|
||||
<MenuItem Header="Menu with Sub _Menu">
|
||||
<MenuItem Header="Submenu _1" />
|
||||
<MenuItem Header="Submenu _2 with Submenu">
|
||||
<MenuItem Header="Submenu Level 2" />
|
||||
</MenuItem>
|
||||
<MenuItem Header="Submenu _3 with Submenu Disabled" IsEnabled="False">
|
||||
<MenuItem Header="Submenu Level 2" />
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
|
||||
<MenuItem.Icon>
|
||||
<Image Source="/Assets/github_icon.png" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Menu Item with _Checkbox" ToggleType="CheckBox" />
|
||||
</MenuItem>
|
||||
<MenuItem Header="_Second">
|
||||
<MenuItem Header="Second _Menu Item" />
|
||||
<MenuItem IsChecked="True" Header="Second _Menu toggle item" ToggleType="CheckBox" />
|
||||
<Separator />
|
||||
<MenuItem GroupName="A" Header="Radio 1 - group" ToggleType="Radio" />
|
||||
<MenuItem IsChecked="True" GroupName="A" Header="Radio 2 - group" ToggleType="Radio" />
|
||||
<MenuItem GroupName="A" Header="Radio 3 - group" ToggleType="Radio">
|
||||
<MenuItem Header="Radio 4 - group" ToggleType="Radio" GroupName="A" />
|
||||
<MenuItem Header="Radio 5 - group" ToggleType="Radio" GroupName="A" />
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem Header="Radio 1" ToggleType="Radio" />
|
||||
<MenuItem IsChecked="True" Header="Radio 2" ToggleType="Radio" />
|
||||
<MenuItem Header="Radio 3" ToggleType="Radio">
|
||||
<MenuItem Header="Radio 4" ToggleType="Radio" />
|
||||
<MenuItem Header="Radio 5" ToggleType="Radio" />
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Thir_d">
|
||||
<MenuItem Header="About"/>
|
||||
<MenuItem Header="_Child">
|
||||
<MenuItem Header="_Grandchild"/>
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</StackPanel>
|
||||
|
||||
</WrapPanel>
|
||||
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Classes="h2">Accelerator Support</TextBlock>
|
||||
|
||||
<TabControl Margin="10" BorderBrush="Gray" BorderThickness="1">
|
||||
<TabItem Header="_Tab 1">
|
||||
<StackPanel>
|
||||
<TextBlock Margin="5">This is tab 1 content</TextBlock>
|
||||
<Label Name="Tab1Label1" Target="Tab1TextBox1">_Label Tab1Label1</Label>
|
||||
<TextBox Name="Tab1TextBox1" Margin="5">This is tab 1 content</TextBox>
|
||||
<Label Name="Tab1Label2" Target="Tab1TextBox2">Label _Tab1Label2</Label>
|
||||
<TextBox Name="Tab1TextBox2" Margin="5">This is tab 1 content</TextBox>
|
||||
</StackPanel>
|
||||
|
||||
</TabItem>
|
||||
<TabItem Header="T_ab 2">
|
||||
<TextBlock Margin="5">This is tab 2 content</TextBlock>
|
||||
</TabItem>
|
||||
<TabItem Header="_Tab 3">
|
||||
|
||||
</TabItem>
|
||||
<TabItem Header="_Tab 4">
|
||||
<TextBlock Margin="5">This is tab 4 content</TextBlock>
|
||||
</TabItem>
|
||||
<TabItem Header="_Fab 5">
|
||||
<TextBlock Margin="5">This is fab 5 content</TextBlock>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
<StackPanel Spacing="10">
|
||||
<Label Name="Label0">Label with Ac_celerator 'C' and no Target</Label>
|
||||
<TextBox Name="TextBox0" Text="Some Text"></TextBox>
|
||||
|
||||
<Label Name="Label1" Target="TextBox1">_Label with Accelerator 'L'</Label>
|
||||
<TextBox Name="TextBox1" Text="Some Text"></TextBox>
|
||||
|
||||
<Label Name="Label2" Target="TextBox2">La_bel with Accelerator 'B'</Label>
|
||||
<TextBox Name="TextBox2" Text="Some Text"></TextBox>
|
||||
|
||||
<Label Name="Label3" Target="TextBox3">L_abel with Accelerator 'A'</Label>
|
||||
<TextBox Name="TextBox3" Text="Some Text"></TextBox>
|
||||
|
||||
<Label Name="Label4" Target="TextBox4">La_bel with Accelerator 'B'</Label>
|
||||
<TextBox Name="TextBox4" Text="Some Text"></TextBox>
|
||||
|
||||
<Label Name="Label5" Target="TextBox5">_Flabel with Accelerator 'F' (Same as in Menu > File)</Label>
|
||||
<TextBox Name="TextBox5" Text="Some Text"></TextBox>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="10" Orientation="Horizontal">
|
||||
<Button Name="Button1">_Button 1</Button>
|
||||
<Button Name="Button2">_Button 2</Button>
|
||||
<Button Name="Button3">_Button 3</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</UserControl>
|
|
@ -0,0 +1,18 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace ControlCatalog.Pages
|
||||
{
|
||||
public class AcceleratorPage : UserControl
|
||||
{
|
||||
public AcceleratorPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.LogicalTree;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
namespace Avalonia.Input
|
||||
{
|
||||
|
@ -11,11 +12,20 @@ namespace Avalonia.Input
|
|||
/// </summary>
|
||||
internal class AccessKeyHandler : IAccessKeyHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the AccessKey attached event.
|
||||
/// </summary>
|
||||
public static readonly RoutedEvent<AccessKeyEventArgs> AccessKeyEvent =
|
||||
RoutedEvent.Register<AccessKeyEventArgs>(
|
||||
"AccessKey",
|
||||
RoutingStrategies.Bubble,
|
||||
typeof(AccessKeyHandler));
|
||||
|
||||
/// <summary>
|
||||
/// Defines the AccessKeyPressed attached event.
|
||||
/// </summary>
|
||||
public static readonly RoutedEvent<RoutedEventArgs> AccessKeyPressedEvent =
|
||||
RoutedEvent.Register<RoutedEventArgs>(
|
||||
public static readonly RoutedEvent<AccessKeyPressedEventArgs> AccessKeyPressedEvent =
|
||||
RoutedEvent.Register<AccessKeyPressedEventArgs>(
|
||||
"AccessKeyPressed",
|
||||
RoutingStrategies.Bubble,
|
||||
typeof(AccessKeyHandler));
|
||||
|
@ -23,7 +33,9 @@ namespace Avalonia.Input
|
|||
/// <summary>
|
||||
/// The registered access keys.
|
||||
/// </summary>
|
||||
private readonly List<(string AccessKey, IInputElement Element)> _registered = new();
|
||||
private readonly List<AccessKeyRegistration> _registrations = [];
|
||||
|
||||
protected IReadOnlyList<AccessKeyRegistration> Registrations => _registrations;
|
||||
|
||||
/// <summary>
|
||||
/// The window to which the handler belongs.
|
||||
|
@ -48,7 +60,7 @@ namespace Avalonia.Input
|
|||
/// <summary>
|
||||
/// Element to restore following AltKey taking focus.
|
||||
/// </summary>
|
||||
private IInputElement? _restoreFocusElement;
|
||||
private WeakReference<IInputElement>? _restoreFocusElementRef;
|
||||
|
||||
/// <summary>
|
||||
/// The window's main menu.
|
||||
|
@ -97,6 +109,12 @@ namespace Avalonia.Input
|
|||
_owner.AddHandler(InputElement.KeyDownEvent, OnKeyDown, RoutingStrategies.Bubble);
|
||||
_owner.AddHandler(InputElement.KeyUpEvent, OnPreviewKeyUp, RoutingStrategies.Tunnel);
|
||||
_owner.AddHandler(InputElement.PointerPressedEvent, OnPreviewPointerPressed, RoutingStrategies.Tunnel);
|
||||
|
||||
OnSetOwner(owner);
|
||||
}
|
||||
|
||||
protected virtual void OnSetOwner(IInputRoot owner)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -106,14 +124,19 @@ namespace Avalonia.Input
|
|||
/// <param name="element">The input element.</param>
|
||||
public void Register(char accessKey, IInputElement element)
|
||||
{
|
||||
var existing = _registered.FirstOrDefault(x => x.Item2 == element);
|
||||
|
||||
if (existing != default)
|
||||
var key = NormalizeKey(accessKey.ToString());
|
||||
|
||||
// remove dead elements with matching key
|
||||
for (var i = _registrations.Count - 1; i >= 0; i--)
|
||||
{
|
||||
_registered.Remove(existing);
|
||||
var registration = _registrations[i];
|
||||
if (registration.Key == key && registration.GetInputElement() == null)
|
||||
{
|
||||
_registrations.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
_registered.Add((accessKey.ToString().ToUpperInvariant(), element));
|
||||
_registrations.Add(new AccessKeyRegistration(key, new WeakReference<IInputElement>(element)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -122,9 +145,15 @@ namespace Avalonia.Input
|
|||
/// <param name="element">The input element.</param>
|
||||
public void Unregister(IInputElement element)
|
||||
{
|
||||
foreach (var i in _registered.Where(x => x.Item2 == element).ToList())
|
||||
// remove element and all dead elements
|
||||
for (var i = _registrations.Count - 1; i >= 0; i--)
|
||||
{
|
||||
_registered.Remove(i);
|
||||
var registration = _registrations[i];
|
||||
var inputElement = registration.GetInputElement();
|
||||
if (inputElement == null || inputElement == element)
|
||||
{
|
||||
_registrations.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,21 +164,29 @@ namespace Avalonia.Input
|
|||
/// <param name="e">The event args.</param>
|
||||
protected virtual void OnPreviewKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.LeftAlt || e.Key == Key.RightAlt)
|
||||
// if the owner (IInputRoot) does not have the keyboard focus, ignore all keyboard events
|
||||
// KeyboardDevice.IsKeyboardFocusWithin in case of a PopupRoot seems to only work once, so we created our own
|
||||
var isFocusWithinOwner = IsFocusWithinOwner(_owner!);
|
||||
if (!isFocusWithinOwner)
|
||||
return;
|
||||
|
||||
if (e.Key is Key.LeftAlt or Key.RightAlt)
|
||||
{
|
||||
_altIsDown = true;
|
||||
|
||||
if (MainMenu == null || !MainMenu.IsOpen)
|
||||
if (MainMenu is not { IsOpen: true })
|
||||
{
|
||||
var focusManager = FocusManager.GetFocusManager(e.Source as IInputElement);
|
||||
|
||||
|
||||
// TODO: Use FocusScopes to store the current element and restore it when context menu is closed.
|
||||
// Save currently focused input element.
|
||||
_restoreFocusElement = focusManager?.GetFocusedElement();
|
||||
var focusedElement = focusManager?.GetFocusedElement();
|
||||
if (focusedElement is not null)
|
||||
_restoreFocusElementRef = new WeakReference<IInputElement>(focusedElement);
|
||||
|
||||
// When Alt is pressed without a main menu, or with a closed main menu, show
|
||||
// access key markers in the window (i.e. "_File").
|
||||
_owner!.ShowAccessKeys = _showingAccessKeys = true;
|
||||
_owner!.ShowAccessKeys = _showingAccessKeys = isFocusWithinOwner;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -157,8 +194,11 @@ namespace Avalonia.Input
|
|||
CloseMenu();
|
||||
_ignoreAltUp = true;
|
||||
|
||||
_restoreFocusElement?.Focus();
|
||||
_restoreFocusElement = null;
|
||||
if (_restoreFocusElementRef?.TryGetTarget(out var restoreElement) ?? false)
|
||||
{
|
||||
restoreElement.Focus();
|
||||
}
|
||||
_restoreFocusElementRef = null;
|
||||
}
|
||||
}
|
||||
else if (_altIsDown)
|
||||
|
@ -174,35 +214,20 @@ namespace Avalonia.Input
|
|||
/// <param name="e">The event args.</param>
|
||||
protected virtual void OnKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
bool menuIsOpen = MainMenu?.IsOpen == true;
|
||||
// if the owner (IInputRoot) does not have the keyboard focus, ignore all keyboard events
|
||||
// KeyboardDevice.IsKeyboardFocusWithin in case of a PopupRoot seems to only work once, so we created our own
|
||||
var isFocusWithinOwner = IsFocusWithinOwner(_owner!);
|
||||
if (!isFocusWithinOwner)
|
||||
return;
|
||||
|
||||
if (e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) && !e.KeyModifiers.HasAllFlags(KeyModifiers.Control) || menuIsOpen)
|
||||
{
|
||||
// If any other key is pressed with the Alt key held down, or the main menu is open,
|
||||
// find all controls who have registered that access key.
|
||||
var text = e.Key.ToString();
|
||||
var matches = _registered
|
||||
.Where(x => string.Equals(x.AccessKey, text, StringComparison.OrdinalIgnoreCase)
|
||||
&& x.Element.IsEffectivelyVisible
|
||||
&& x.Element.IsEffectivelyEnabled)
|
||||
.Select(x => x.Element);
|
||||
if ((!e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) || e.KeyModifiers.HasAllFlags(KeyModifiers.Control)) &&
|
||||
MainMenu?.IsOpen != true)
|
||||
return;
|
||||
|
||||
// If the menu is open, only match controls in the menu's visual tree.
|
||||
if (menuIsOpen)
|
||||
{
|
||||
matches = matches.Where(x => x is not null && ((Visual)MainMenu!).IsLogicalAncestorOf((Visual)x));
|
||||
}
|
||||
|
||||
var match = matches.FirstOrDefault();
|
||||
|
||||
// If there was a match, raise the AccessKeyPressed event on it.
|
||||
if (match is not null)
|
||||
{
|
||||
match.RaiseEvent(new RoutedEventArgs(AccessKeyPressedEvent));
|
||||
}
|
||||
}
|
||||
e.Handled = ProcessKey(e.Key.ToString(), e.Source as IInputElement);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Alt/F10 keys being released in the window.
|
||||
/// </summary>
|
||||
|
@ -255,5 +280,302 @@ namespace Avalonia.Input
|
|||
{
|
||||
_owner!.ShowAccessKeys = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the given key for the element's targets
|
||||
/// </summary>
|
||||
/// <param name="key">The access key to process.</param>
|
||||
/// <param name="element">The element to get the targets which are in scope.</param>
|
||||
/// <returns>If there matches <c>true</c>, otherwise <c>false</c>.</returns>
|
||||
protected bool ProcessKey(string key, IInputElement? element)
|
||||
{
|
||||
key = NormalizeKey(key);
|
||||
var senderInfo = GetTargetForElement(element, key);
|
||||
// Find the possible targets matching the access key
|
||||
var targets = SortByHierarchy(GetTargetsForKey(key, element, senderInfo));
|
||||
var result = ProcessKey(key, targets);
|
||||
return result != ProcessKeyResult.NoMatch;
|
||||
}
|
||||
|
||||
private static string NormalizeKey(string key) => key.ToUpperInvariant();
|
||||
|
||||
private static ProcessKeyResult ProcessKey(string key, List<IInputElement> targets)
|
||||
{
|
||||
if (!targets.Any())
|
||||
return ProcessKeyResult.NoMatch;
|
||||
|
||||
var isSingleTarget = true;
|
||||
var lastWasFocused = false;
|
||||
|
||||
IInputElement? effectiveTarget = null;
|
||||
|
||||
var chosenIndex = 0;
|
||||
for (var i = 0; i < targets.Count; i++)
|
||||
{
|
||||
var target = targets[i];
|
||||
|
||||
if (!IsTargetable(target))
|
||||
continue;
|
||||
|
||||
if (effectiveTarget == null)
|
||||
{
|
||||
effectiveTarget = target;
|
||||
chosenIndex = i;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (lastWasFocused)
|
||||
{
|
||||
effectiveTarget = target;
|
||||
chosenIndex = i;
|
||||
}
|
||||
|
||||
isSingleTarget = false;
|
||||
}
|
||||
|
||||
lastWasFocused = target.IsFocused;
|
||||
}
|
||||
|
||||
if (effectiveTarget == null)
|
||||
return ProcessKeyResult.NoMatch;
|
||||
|
||||
var args = new AccessKeyEventArgs(key, isMultiple: !isSingleTarget);
|
||||
effectiveTarget.RaiseEvent(args);
|
||||
|
||||
return chosenIndex == targets.Count - 1 ? ProcessKeyResult.LastMatch : ProcessKeyResult.MoreMatches;
|
||||
}
|
||||
|
||||
private List<IInputElement> GetTargetsForKey(string key, IInputElement? sender,
|
||||
AccessKeyInformation senderInfo)
|
||||
{
|
||||
var possibleElements = CopyMatchingAndPurgeDead(key);
|
||||
|
||||
if (!possibleElements.Any())
|
||||
return possibleElements;
|
||||
|
||||
var finalTargets = new List<IInputElement>(1);
|
||||
|
||||
// Go through all the possible elements, find the interesting candidates
|
||||
foreach (var element in possibleElements)
|
||||
{
|
||||
if (element != sender)
|
||||
{
|
||||
if (!IsTargetable(element))
|
||||
continue;
|
||||
|
||||
var elementInfo = GetTargetForElement(element, key);
|
||||
if (elementInfo.Target == null)
|
||||
continue;
|
||||
|
||||
finalTargets.Add(elementInfo.Target);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is the same element that sent the event so it must be in the same scope.
|
||||
// Just add it to the final targets
|
||||
if (senderInfo.Target == null)
|
||||
continue;
|
||||
|
||||
finalTargets.Add(senderInfo.Target);
|
||||
}
|
||||
}
|
||||
|
||||
return finalTargets;
|
||||
}
|
||||
|
||||
private static bool IsTargetable(IInputElement element) =>
|
||||
element is { IsEffectivelyEnabled: true, IsEffectivelyVisible: true };
|
||||
|
||||
private List<IInputElement> CopyMatchingAndPurgeDead(string key)
|
||||
{
|
||||
var matches = new List<IInputElement>(_registrations.Count);
|
||||
|
||||
// collect live elements with matching key and remove dead elements
|
||||
for (var i = _registrations.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var registration = _registrations[i];
|
||||
var inputElement = registration.GetInputElement();
|
||||
if (inputElement != null)
|
||||
{
|
||||
if (registration.Key == key)
|
||||
{
|
||||
matches.Add(inputElement);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_registrations.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
// since we collected the elements when iterating from back to front
|
||||
// we need to reverse them to ensure the original order
|
||||
matches.Reverse();
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns targeting information for the given element.
|
||||
/// </summary>
|
||||
/// <param name="element"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <returns>AccessKeyInformation with target for the access key.</returns>
|
||||
private static AccessKeyInformation GetTargetForElement(IInputElement? element, string key)
|
||||
{
|
||||
var info = new AccessKeyInformation();
|
||||
if (element == null)
|
||||
return info;
|
||||
|
||||
var args = new AccessKeyPressedEventArgs(key);
|
||||
element.RaiseEvent(args);
|
||||
info.Target = args.Target;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the focused element is a descendent of the owner.
|
||||
/// </summary>
|
||||
/// <param name="owner">The owner to check.</param>
|
||||
/// <returns>If focused element is decendant of owner <c>true</c>, otherwise <c>false</c>. </returns>
|
||||
private static bool IsFocusWithinOwner(IInputRoot owner)
|
||||
{
|
||||
var focusedElement = KeyboardDevice.Instance?.FocusedElement;
|
||||
if (focusedElement is not InputElement inputElement)
|
||||
return false;
|
||||
|
||||
var isAncestorOf = owner is Visual root && root.IsVisualAncestorOf(inputElement);
|
||||
return isAncestorOf;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sorts the list of targets according to logical ancestors in the hierarchy
|
||||
/// so that child elements, for example within in the content of a tab,
|
||||
/// are processed before the next parent item i.e. the next tab item.
|
||||
/// </summary>
|
||||
private static List<IInputElement> SortByHierarchy(List<IInputElement> targets)
|
||||
{
|
||||
// bail out, if there are no targets to sort
|
||||
if (targets.Count <= 1)
|
||||
return targets;
|
||||
|
||||
var sorted = new List<IInputElement>(targets.Count);
|
||||
var queue = new Queue<IInputElement>(targets);
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var element = queue.Dequeue();
|
||||
|
||||
// if the element was already added, do nothing
|
||||
if (sorted.Contains(element))
|
||||
continue;
|
||||
|
||||
// add the element itself
|
||||
sorted.Add(element);
|
||||
|
||||
// if the element is not a potential parent, do nothing
|
||||
if (element is not ILogical parentElement)
|
||||
continue;
|
||||
|
||||
// add all descendants of the element
|
||||
sorted.AddRange(queue
|
||||
.Where(child => parentElement
|
||||
.IsLogicalAncestorOf(child as ILogical)));
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
|
||||
private enum ProcessKeyResult
|
||||
{
|
||||
NoMatch,
|
||||
MoreMatches,
|
||||
LastMatch
|
||||
}
|
||||
|
||||
private struct AccessKeyInformation
|
||||
{
|
||||
public IInputElement? Target { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The inputs to an AccessKeyPressedEventHandler
|
||||
/// </summary>
|
||||
internal class AccessKeyPressedEventArgs : RoutedEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The constructor for AccessKeyPressed event args
|
||||
/// </summary>
|
||||
public AccessKeyPressedEventArgs()
|
||||
{
|
||||
RoutedEvent = AccessKeyHandler.AccessKeyPressedEvent;
|
||||
Key = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for AccessKeyPressed event args
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
public AccessKeyPressedEventArgs(string key) : this()
|
||||
{
|
||||
RoutedEvent = AccessKeyHandler.AccessKeyPressedEvent;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Target element for the element that raised this event.
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public IInputElement? Target { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Key that was pressed
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public string? Key { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information pertaining to when the access key associated with an element is pressed
|
||||
/// </summary>
|
||||
internal class AccessKeyEventArgs : RoutedEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
internal AccessKeyEventArgs(string key, bool isMultiple)
|
||||
{
|
||||
RoutedEvent = AccessKeyHandler.AccessKeyEvent;
|
||||
|
||||
Key = key;
|
||||
IsMultiple = isMultiple;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The key that was pressed which invoked this access key
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public string Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Were there other elements which are also invoked by this key
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public bool IsMultiple { get; }
|
||||
}
|
||||
|
||||
internal class AccessKeyRegistration
|
||||
{
|
||||
private readonly WeakReference<IInputElement> _target;
|
||||
public string Key { get; }
|
||||
|
||||
public AccessKeyRegistration(string key, WeakReference<IInputElement> target)
|
||||
{
|
||||
_target = target;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
public IInputElement? GetInputElement() =>
|
||||
_target.TryGetTarget(out var target) ? target : null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Input.GestureRecognizers;
|
||||
using Avalonia.Input.TextInput;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Reactive;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Avalonia.Input
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -231,6 +229,10 @@ namespace Avalonia.Input
|
|||
PointerPressedEvent.AddClassHandler<InputElement>((x, e) => x.OnGesturePointerPressed(e), handledEventsToo: true);
|
||||
PointerReleasedEvent.AddClassHandler<InputElement>((x, e) => x.OnGesturePointerReleased(e), handledEventsToo: true);
|
||||
PointerCaptureLostEvent.AddClassHandler<InputElement>((x, e) => x.OnGesturePointerCaptureLost(e), handledEventsToo: true);
|
||||
|
||||
|
||||
// Access Key Handling
|
||||
AccessKeyHandler.AccessKeyEvent.AddClassHandler<InputElement>((x, e) => x.OnAccessKey(e));
|
||||
}
|
||||
|
||||
public InputElement()
|
||||
|
@ -282,7 +284,7 @@ namespace Avalonia.Input
|
|||
add { AddHandler(TextInputEvent, value); }
|
||||
remove { RemoveHandler(TextInputEvent, value); }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when an input element gains input focus and input method is looking for the corresponding client
|
||||
/// </summary>
|
||||
|
@ -346,7 +348,7 @@ namespace Avalonia.Input
|
|||
add => AddHandler(PointerCaptureLostEvent, value);
|
||||
remove => RemoveHandler(PointerCaptureLostEvent, value);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the mouse is scrolled over the control.
|
||||
/// </summary>
|
||||
|
@ -355,7 +357,7 @@ namespace Avalonia.Input
|
|||
add { AddHandler(PointerWheelChangedEvent, value); }
|
||||
remove { RemoveHandler(PointerWheelChangedEvent, value); }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a tap gesture occurs on the control.
|
||||
/// </summary>
|
||||
|
@ -364,7 +366,7 @@ namespace Avalonia.Input
|
|||
add { AddHandler(TappedEvent, value); }
|
||||
remove { RemoveHandler(TappedEvent, value); }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a hold gesture occurs on the control.
|
||||
/// </summary>
|
||||
|
@ -409,7 +411,7 @@ namespace Avalonia.Input
|
|||
get { return GetValue(CursorProperty); }
|
||||
set { SetValue(CursorProperty, value); }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether keyboard focus is anywhere within the element or its visual tree child elements.
|
||||
/// </summary>
|
||||
|
@ -515,6 +517,17 @@ namespace Avalonia.Input
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method is used to execute the action on an effective IInputElement when a corresponding access key has been invoked.
|
||||
/// By default, the Focus() method is invoked with the NavigationMethod.Tab to indicate a visual focus adorner.
|
||||
/// Overwrite this method if other methods or additional functionality is needed when an item should receive the focus.
|
||||
/// </summary>
|
||||
/// <param name="e">AccessKeyEventArgs are passed on to indicate if there are multiple matches or not.</param>
|
||||
protected virtual void OnAccessKey(RoutedEventArgs e)
|
||||
{
|
||||
Focus(NavigationMethod.Tab);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
|
|
|
@ -104,7 +104,7 @@ namespace Avalonia.Controls
|
|||
static Button()
|
||||
{
|
||||
FocusableProperty.OverrideDefaultValue(typeof(Button), true);
|
||||
AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<Button>((lbl, args) => lbl.OnAccessKey(args));
|
||||
AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<Button>(OnAccessKeyPressed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -199,7 +199,7 @@ namespace Avalonia.Controls
|
|||
|
||||
/// <inheritdoc/>
|
||||
protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
|
@ -278,7 +278,18 @@ namespace Avalonia.Controls
|
|||
}
|
||||
}
|
||||
|
||||
protected virtual void OnAccessKey(RoutedEventArgs e) => OnClick();
|
||||
/// <inheritdoc />
|
||||
protected override void OnAccessKey(RoutedEventArgs e)
|
||||
{
|
||||
if (e is AccessKeyEventArgs { IsMultiple: true })
|
||||
{
|
||||
base.OnAccessKey(e);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnClick();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
|
@ -563,6 +574,14 @@ namespace Avalonia.Controls
|
|||
|
||||
internal void PerformClick() => OnClick();
|
||||
|
||||
private static void OnAccessKeyPressed(Button sender, AccessKeyPressedEventArgs e)
|
||||
{
|
||||
if (e.Handled || e.Target is not null)
|
||||
return;
|
||||
e.Target = sender;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the <see cref="ICommand.CanExecuteChanged"/> event fires.
|
||||
/// </summary>
|
||||
|
|
|
@ -28,9 +28,8 @@ namespace Avalonia.Controls
|
|||
|
||||
static Label()
|
||||
{
|
||||
AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<Label>((lbl, args) => lbl.LabelActivated(args));
|
||||
// IsTabStopProperty.OverrideDefaultValue<Label>(false)
|
||||
FocusableProperty.OverrideDefaultValue<Label>(false);
|
||||
AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<Label>(OnAccessKeyPressed);
|
||||
IsTabStopProperty.OverrideDefaultValue<Label>(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -40,13 +39,11 @@ namespace Avalonia.Controls
|
|||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method which focuses <see cref="Target"/> input element
|
||||
/// </summary>
|
||||
private void LabelActivated(RoutedEventArgs args)
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAccessKey(RoutedEventArgs e)
|
||||
{
|
||||
Target?.Focus();
|
||||
args.Handled = Target != null;
|
||||
LabelActivated(e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -62,9 +59,25 @@ namespace Avalonia.Controls
|
|||
base.OnPointerPressed(e);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override AutomationPeer OnCreateAutomationPeer()
|
||||
{
|
||||
return new LabelAutomationPeer(this);
|
||||
}
|
||||
|
||||
private void LabelActivated(RoutedEventArgs e)
|
||||
{
|
||||
Target?.Focus();
|
||||
e.Handled = Target != null;
|
||||
}
|
||||
|
||||
private static void OnAccessKeyPressed(Label label, AccessKeyPressedEventArgs e)
|
||||
{
|
||||
if (e is not { Handled: false, Target: null })
|
||||
return;
|
||||
|
||||
e.Target = label.Target;
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,8 +40,9 @@ namespace Avalonia.Controls
|
|||
KeyboardNavigationMode.Once);
|
||||
AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue<Menu>(AccessibilityView.Control);
|
||||
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<Menu>(AutomationControlType.Menu);
|
||||
AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<Menu>(OnAccessKeyPressed);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Close()
|
||||
{
|
||||
|
@ -104,5 +105,14 @@ namespace Avalonia.Controls
|
|||
if ((element as MenuItem)?.ItemContainerTheme == ItemContainerTheme)
|
||||
element.ClearValue(ItemContainerThemeProperty);
|
||||
}
|
||||
|
||||
private static void OnAccessKeyPressed(Menu sender, AccessKeyPressedEventArgs e)
|
||||
{
|
||||
if (e.Handled || e.Source is not StyledElement target)
|
||||
return;
|
||||
|
||||
e.Target = DefaultMenuInteractionHandler.GetMenuItemCore(target);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -143,6 +143,7 @@ namespace Avalonia.Controls
|
|||
ClickEvent.AddClassHandler<MenuItem>((x, e) => x.OnClick(e));
|
||||
SubmenuOpenedEvent.AddClassHandler<MenuItem>((x, e) => x.OnSubmenuOpened(e));
|
||||
AutomationProperties.IsOffscreenBehaviorProperty.OverrideDefaultValue<MenuItem>(IsOffscreenBehavior.FromClip);
|
||||
AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<MenuItem>(OnAccessKeyPressed);
|
||||
}
|
||||
|
||||
public MenuItem()
|
||||
|
@ -565,7 +566,7 @@ namespace Avalonia.Controls
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Closes all submenus of the menu item.
|
||||
/// </summary>
|
||||
|
@ -603,6 +604,15 @@ namespace Avalonia.Controls
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
private static void OnAccessKeyPressed(MenuItem sender, AccessKeyPressedEventArgs e)
|
||||
{
|
||||
if (e is not { Handled: false, Target: null })
|
||||
return;
|
||||
|
||||
e.Target = sender;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the <see cref="CommandParameter"/> property changes.
|
||||
|
|
|
@ -1,82 +1,17 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace Avalonia.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles access keys within a <see cref="MenuItem"/>
|
||||
/// </summary>
|
||||
internal class MenuItemAccessKeyHandler : IAccessKeyHandler
|
||||
internal class MenuItemAccessKeyHandler : AccessKeyHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// The registered access keys.
|
||||
/// </summary>
|
||||
private readonly List<(string AccessKey, IInputElement Element)> _registered = new();
|
||||
|
||||
/// <summary>
|
||||
/// The window to which the handler belongs.
|
||||
/// </summary>
|
||||
private IInputRoot? _owner;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the window's main menu.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This property is ignored as a menu item cannot have a main menu.
|
||||
/// </remarks>
|
||||
public IMainMenu? MainMenu { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the owner of the access key handler.
|
||||
/// </summary>
|
||||
/// <param name="owner">The owner.</param>
|
||||
/// <remarks>
|
||||
/// This method can only be called once, typically by the owner itself on creation.
|
||||
/// </remarks>
|
||||
public void SetOwner(IInputRoot owner)
|
||||
protected override void OnSetOwner(IInputRoot owner)
|
||||
{
|
||||
_ = owner ?? throw new ArgumentNullException(nameof(owner));
|
||||
|
||||
if (_owner != null)
|
||||
{
|
||||
throw new InvalidOperationException("AccessKeyHandler owner has already been set.");
|
||||
}
|
||||
|
||||
_owner = owner;
|
||||
|
||||
_owner.AddHandler(InputElement.TextInputEvent, OnTextInput);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an input element to be associated with an access key.
|
||||
/// </summary>
|
||||
/// <param name="accessKey">The access key.</param>
|
||||
/// <param name="element">The input element.</param>
|
||||
public void Register(char accessKey, IInputElement element)
|
||||
{
|
||||
var existing = _registered.FirstOrDefault(x => x.Item2 == element);
|
||||
|
||||
if (existing != default)
|
||||
{
|
||||
_registered.Remove(existing);
|
||||
}
|
||||
|
||||
_registered.Add((accessKey.ToString().ToUpperInvariant(), element));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters the access keys associated with the input element.
|
||||
/// </summary>
|
||||
/// <param name="element">The input element.</param>
|
||||
public void Unregister(IInputElement element)
|
||||
{
|
||||
foreach (var i in _registered.Where(x => x.Item2 == element).ToList())
|
||||
{
|
||||
_registered.Remove(i);
|
||||
}
|
||||
owner.AddHandler(InputElement.TextInputEvent, OnTextInput);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -86,17 +21,17 @@ namespace Avalonia.Controls
|
|||
/// <param name="e">The event args.</param>
|
||||
protected virtual void OnTextInput(object? sender, TextInputEventArgs e)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(e.Text))
|
||||
{
|
||||
var text = e.Text;
|
||||
var focus = _registered
|
||||
.FirstOrDefault(x => string.Equals(x.AccessKey, text, StringComparison.OrdinalIgnoreCase)
|
||||
&& x.Element.IsEffectivelyVisible).Element;
|
||||
if (string.IsNullOrWhiteSpace(e.Text))
|
||||
return;
|
||||
|
||||
var key = e.Text;
|
||||
var registration = Registrations
|
||||
.FirstOrDefault(x => string.Equals(x.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (registration == null)
|
||||
return;
|
||||
|
||||
focus?.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent));
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
e.Handled = ProcessKey(key, registration.GetInputElement());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,12 +80,16 @@ namespace Avalonia.Controls.Platform
|
|||
protected internal virtual void AccessKeyPressed(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var item = GetMenuItemCore(e.Source as Control);
|
||||
if (item is null)
|
||||
return;
|
||||
|
||||
if (item == null)
|
||||
if (e is AccessKeyEventArgs { IsMultiple: true })
|
||||
{
|
||||
// in case we have multiple matches, only focus item and bail
|
||||
item.Focus(NavigationMethod.Tab);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (item.HasSubMenu && item.IsEffectivelyEnabled)
|
||||
{
|
||||
Open(item, true);
|
||||
|
@ -290,7 +294,7 @@ namespace Avalonia.Controls.Platform
|
|||
Menu.KeyDown += KeyDown;
|
||||
Menu.PointerPressed += PointerPressed;
|
||||
Menu.PointerReleased += PointerReleased;
|
||||
Menu.AddHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed);
|
||||
Menu.AddHandler(AccessKeyHandler.AccessKeyEvent, AccessKeyPressed);
|
||||
Menu.AddHandler(MenuBase.OpenedEvent, MenuOpened);
|
||||
Menu.AddHandler(MenuItem.PointerEnteredItemEvent, PointerEntered);
|
||||
Menu.AddHandler(MenuItem.PointerExitedItemEvent, PointerExited);
|
||||
|
@ -332,7 +336,7 @@ namespace Avalonia.Controls.Platform
|
|||
Menu.KeyDown -= KeyDown;
|
||||
Menu.PointerPressed -= PointerPressed;
|
||||
Menu.PointerReleased -= PointerReleased;
|
||||
Menu.RemoveHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed);
|
||||
Menu.RemoveHandler(AccessKeyHandler.AccessKeyEvent, AccessKeyPressed);
|
||||
Menu.RemoveHandler(MenuBase.OpenedEvent, MenuOpened);
|
||||
Menu.RemoveHandler(MenuItem.PointerEnteredItemEvent, PointerEntered);
|
||||
Menu.RemoveHandler(MenuItem.PointerExitedItemEvent, PointerExited);
|
||||
|
|
|
@ -40,7 +40,7 @@ namespace Avalonia.Controls
|
|||
DataContextProperty.Changed.AddClassHandler<TabItem>((x, e) => x.UpdateHeader(e));
|
||||
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<TabItem>(AutomationControlType.TabItem);
|
||||
AutomationProperties.IsOffscreenBehaviorProperty.OverrideDefaultValue<TabItem>(IsOffscreenBehavior.FromClip);
|
||||
AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<TabItem>((tabItem, args) => tabItem.TabItemActivated(args));
|
||||
AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<TabItem>(OnAccessKeyPressed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -61,13 +61,31 @@ namespace Avalonia.Controls
|
|||
set => SetValue(IsSelectedProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAccessKey(RoutedEventArgs e)
|
||||
{
|
||||
Focus();
|
||||
SetCurrentValue(IsSelectedProperty, true);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
protected override AutomationPeer OnCreateAutomationPeer() => new ListItemAutomationPeer(this);
|
||||
|
||||
|
||||
[Obsolete("Owner manages its children properties by itself")]
|
||||
protected void SubscribeToOwnerProperties(AvaloniaObject owner)
|
||||
{
|
||||
}
|
||||
|
||||
private static void OnAccessKeyPressed(TabItem tabItem, AccessKeyPressedEventArgs e)
|
||||
{
|
||||
if (e.Handled || (e.Target != null && tabItem.IsSelected))
|
||||
return;
|
||||
|
||||
e.Target = tabItem;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj)
|
||||
{
|
||||
if (Header == null)
|
||||
|
@ -95,11 +113,5 @@ namespace Avalonia.Controls
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TabItemActivated(RoutedEventArgs args)
|
||||
{
|
||||
SetCurrentValue(IsSelectedProperty, true);
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -142,68 +142,85 @@ namespace Avalonia.Base.UnitTests.Input
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Raise_AccessKeyPressed_For_Registered_Access_Key()
|
||||
public void Should_Raise_AccessKey_For_Registered_Access_Key()
|
||||
{
|
||||
var button = new Button();
|
||||
var root = new TestRoot(button);
|
||||
var target = new AccessKeyHandler();
|
||||
var raised = 0;
|
||||
using (UnitTestApplication.Start(TestServices.RealFocus))
|
||||
{
|
||||
var button = new Button();
|
||||
var root = new TestRoot(button);
|
||||
var target = new AccessKeyHandler();
|
||||
var raised = 0;
|
||||
|
||||
target.SetOwner(root);
|
||||
target.Register('A', button);
|
||||
button.AddHandler(AccessKeyHandler.AccessKeyPressedEvent, (s, e) => ++raised);
|
||||
KeyboardDevice.Instance?.SetFocusedElement(button, NavigationMethod.Unspecified, KeyModifiers.None);
|
||||
|
||||
KeyDown(root, Key.LeftAlt);
|
||||
Assert.Equal(0, raised);
|
||||
target.SetOwner(root);
|
||||
target.Register('A', button);
|
||||
button.AddHandler(AccessKeyHandler.AccessKeyEvent, (s, e) => ++raised);
|
||||
|
||||
KeyDown(root, Key.A, KeyModifiers.Alt);
|
||||
Assert.Equal(1, raised);
|
||||
KeyDown(root, Key.LeftAlt);
|
||||
Assert.Equal(0, raised);
|
||||
|
||||
KeyUp(root, Key.A, KeyModifiers.Alt);
|
||||
KeyUp(root, Key.LeftAlt);
|
||||
KeyDown(root, Key.A, KeyModifiers.Alt);
|
||||
Assert.Equal(1, raised);
|
||||
|
||||
Assert.Equal(1, raised);
|
||||
KeyUp(root, Key.A, KeyModifiers.Alt);
|
||||
KeyUp(root, Key.LeftAlt);
|
||||
|
||||
Assert.Equal(1, raised);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Not_Raise_AccessKeyPressed_For_Registered_Access_Key_When_Not_Effectively_Enabled()
|
||||
[Theory]
|
||||
[InlineData(false, 0)]
|
||||
[InlineData(true, 1)]
|
||||
public void Should_Raise_AccessKey_For_Registered_Access_Key_When_Effectively_Enabled(bool enabled, int expected)
|
||||
{
|
||||
var button = new Button();
|
||||
var root = new TestRoot(button) { IsEnabled = false };
|
||||
var target = new AccessKeyHandler();
|
||||
var raised = 0;
|
||||
using (UnitTestApplication.Start(TestServices.RealFocus))
|
||||
{
|
||||
var button = new Button();
|
||||
var root = new TestRoot(button) { IsEnabled = enabled };
|
||||
var target = new AccessKeyHandler();
|
||||
var raised = 0;
|
||||
|
||||
KeyboardDevice.Instance?.SetFocusedElement(button, NavigationMethod.Unspecified, KeyModifiers.None);
|
||||
|
||||
target.SetOwner(root);
|
||||
target.Register('A', button);
|
||||
button.AddHandler(AccessKeyHandler.AccessKeyEvent, (s, e) => ++raised);
|
||||
|
||||
target.SetOwner(root);
|
||||
target.Register('A', button);
|
||||
button.AddHandler(AccessKeyHandler.AccessKeyPressedEvent, (s, e) => ++raised);
|
||||
KeyDown(root, Key.LeftAlt);
|
||||
Assert.Equal(0, raised);
|
||||
|
||||
KeyDown(root, Key.LeftAlt);
|
||||
Assert.Equal(0, raised);
|
||||
KeyDown(root, Key.A, KeyModifiers.Alt);
|
||||
Assert.Equal(expected, raised);
|
||||
|
||||
KeyDown(root, Key.A, KeyModifiers.Alt);
|
||||
Assert.Equal(0, raised);
|
||||
|
||||
KeyUp(root, Key.A, KeyModifiers.Alt);
|
||||
KeyUp(root, Key.LeftAlt);
|
||||
|
||||
Assert.Equal(0, raised);
|
||||
KeyUp(root, Key.A, KeyModifiers.Alt);
|
||||
KeyUp(root, Key.LeftAlt);
|
||||
Assert.Equal(expected, raised);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Open_MainMenu_On_Alt_KeyUp()
|
||||
{
|
||||
var root = new TestRoot();
|
||||
var target = new AccessKeyHandler();
|
||||
var menu = new Mock<IMainMenu>();
|
||||
using (UnitTestApplication.Start(TestServices.RealFocus))
|
||||
{
|
||||
var target = new AccessKeyHandler();
|
||||
var menu = new FakeMenu();
|
||||
var root = new TestRoot(menu);
|
||||
|
||||
target.SetOwner(root);
|
||||
target.MainMenu = menu.Object;
|
||||
KeyboardDevice.Instance?.SetFocusedElement(menu, NavigationMethod.Unspecified,
|
||||
KeyModifiers.None);
|
||||
|
||||
KeyDown(root, Key.LeftAlt);
|
||||
menu.Verify(x => x.Open(), Times.Never);
|
||||
target.SetOwner(root);
|
||||
target.MainMenu = menu;
|
||||
|
||||
KeyUp(root, Key.LeftAlt);
|
||||
menu.Verify(x => x.Open(), Times.Once);
|
||||
KeyDown(root, Key.LeftAlt);
|
||||
Assert.Equal(0, menu.TimesOpenCalled);
|
||||
|
||||
KeyUp(root, Key.LeftAlt);
|
||||
Assert.Equal(1, menu.TimesOpenCalled);
|
||||
}
|
||||
}
|
||||
|
||||
private static void KeyDown(IInputElement target, Key key, KeyModifiers modifiers = KeyModifiers.None)
|
||||
|
@ -225,5 +242,15 @@ namespace Avalonia.Base.UnitTests.Input
|
|||
KeyModifiers = modifiers,
|
||||
});
|
||||
}
|
||||
|
||||
class FakeMenu : Menu
|
||||
{
|
||||
public int TimesOpenCalled { get; set; }
|
||||
|
||||
public override void Open()
|
||||
{
|
||||
TimesOpenCalled++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
using System;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.UnitTests;
|
||||
using Xunit;
|
||||
|
||||
|
@ -177,10 +175,10 @@ namespace Avalonia.Base.UnitTests.Input
|
|||
};
|
||||
|
||||
target.Focus();
|
||||
target.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent));
|
||||
target.RaiseEvent(new AccessKeyEventArgs("b1", false));
|
||||
Assert.False(target.IsEnabled);
|
||||
Assert.False(target.IsFocused);
|
||||
target1.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent));
|
||||
target1.RaiseEvent(new AccessKeyEventArgs("b2", false));
|
||||
Assert.True(target.IsEnabled);
|
||||
Assert.False(target.IsFocused);
|
||||
}
|
||||
|
|
|
@ -293,9 +293,9 @@ namespace Avalonia.Controls.UnitTests
|
|||
var raised = 0;
|
||||
|
||||
target.Click += (s, e) => ++raised;
|
||||
|
||||
target.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent));
|
||||
|
||||
|
||||
target.RaiseEvent(new AccessKeyEventArgs("b", false));
|
||||
|
||||
Assert.Equal(1, raised);
|
||||
}
|
||||
|
||||
|
@ -304,7 +304,12 @@ namespace Avalonia.Controls.UnitTests
|
|||
{
|
||||
var raised = 0;
|
||||
var ah = new AccessKeyHandler();
|
||||
using var app = UnitTestApplication.Start(TestServices.StyledWindow.With(accessKeyHandler: ah));
|
||||
var kd = new KeyboardDevice();
|
||||
using var app = UnitTestApplication.Start(TestServices.StyledWindow
|
||||
.With(
|
||||
accessKeyHandler: ah,
|
||||
keyboardDevice: () => kd)
|
||||
);
|
||||
|
||||
var impl = CreateMockTopLevelImpl();
|
||||
var command = new TestCommand(p => p is bool value && value, _ => raised++);
|
||||
|
@ -329,6 +334,8 @@ namespace Avalonia.Controls.UnitTests
|
|||
})
|
||||
},
|
||||
};
|
||||
kd.SetFocusedElement(target, NavigationMethod.Unspecified, KeyModifiers.None);
|
||||
|
||||
|
||||
root.ApplyTemplate();
|
||||
root.Presenter.UpdateChild();
|
||||
|
@ -349,7 +356,7 @@ namespace Avalonia.Controls.UnitTests
|
|||
RaiseAccessKey(root, accessKey);
|
||||
|
||||
Assert.Equal(1, raised);
|
||||
|
||||
|
||||
static FuncControlTemplate<TestTopLevel> CreateTemplate()
|
||||
{
|
||||
return new FuncControlTemplate<TestTopLevel>((x, scope) =>
|
||||
|
@ -409,7 +416,7 @@ namespace Avalonia.Controls.UnitTests
|
|||
target.IsEnabled = false;
|
||||
target.Click += (s, e) => ++raised;
|
||||
|
||||
target.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent));
|
||||
target.RaiseEvent(new AccessKeyEventArgs("b", false));
|
||||
|
||||
Assert.Equal(0, raised);
|
||||
}
|
||||
|
|
|
@ -724,7 +724,12 @@ namespace Avalonia.Controls.UnitTests
|
|||
public void Should_TabControl_Recognizes_AccessKey(Key accessKey, int selectedTabIndex)
|
||||
{
|
||||
var ah = new AccessKeyHandler();
|
||||
using (UnitTestApplication.Start(TestServices.StyledWindow.With(accessKeyHandler: ah)))
|
||||
var kd = new KeyboardDevice();
|
||||
using (UnitTestApplication.Start(TestServices.StyledWindow
|
||||
.With(
|
||||
accessKeyHandler: ah,
|
||||
keyboardDevice: () => kd)
|
||||
))
|
||||
{
|
||||
var impl = CreateMockTopLevelImpl();
|
||||
|
||||
|
@ -742,7 +747,8 @@ namespace Avalonia.Controls.UnitTests
|
|||
new TabItem { Header = "_Disabled", IsEnabled = false },
|
||||
}
|
||||
};
|
||||
|
||||
kd.SetFocusedElement((TabItem)tabControl.Items[selectedTabIndex], NavigationMethod.Unspecified, KeyModifiers.None);
|
||||
|
||||
var root = new TestTopLevel(impl.Object)
|
||||
{
|
||||
Template = CreateTemplate(),
|
||||
|
|
|
@ -17,7 +17,7 @@ namespace Avalonia.Markup.UnitTests.Data
|
|||
DataContext = vm,
|
||||
[!Button.CommandProperty] = new Binding("MyMethod"),
|
||||
};
|
||||
target.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent));
|
||||
target.RaiseEvent(new AccessKeyEventArgs("b", false));
|
||||
|
||||
Assert.False(vm.IsSet);
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче