diff --git a/components/Behaviors/src/Keyboard/KeyDownTriggerBehavior.cs b/components/Behaviors/src/Keyboard/KeyDownTriggerBehavior.cs index 2eddac0c..6b213f2a 100644 --- a/components/Behaviors/src/Keyboard/KeyDownTriggerBehavior.cs +++ b/components/Behaviors/src/Keyboard/KeyDownTriggerBehavior.cs @@ -2,28 +2,35 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.UI.Input; using Microsoft.Xaml.Interactivity; using Windows.System; +using Windows.UI.Core; namespace CommunityToolkit.WinUI.Behaviors; /// -/// This behavior listens to a key down event on the associated when it is loaded and executes an action. +/// A behavior that listens to on the associated +/// and executes its actions when the specified key and +/// optional modifier keys are pressed. Supports capturing handled events. /// [TypeConstraint(typeof(FrameworkElement))] public class KeyDownTriggerBehavior : Trigger { + private KeyEventHandler? _handler; + /// - /// Identifies the property. + /// Identifies the dependency property. /// - public static readonly DependencyProperty KeyProperty = DependencyProperty.Register( - nameof(Key), - typeof(VirtualKey), - typeof(KeyDownTriggerBehavior), - new PropertyMetadata(null)); + public static readonly DependencyProperty KeyProperty = + DependencyProperty.Register( + nameof(Key), + typeof(VirtualKey), + typeof(KeyDownTriggerBehavior), + new PropertyMetadata(VirtualKey.None)); /// - /// Gets or sets the key to listen when the associated object is loaded. + /// Gets or sets the key that triggers the behavior. /// public VirtualKey Key { @@ -31,30 +38,146 @@ public VirtualKey Key set => SetValue(KeyProperty, value); } + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ModifiersProperty = + DependencyProperty.Register( + nameof(Modifiers), + typeof(VirtualKeyModifiers), + typeof(KeyDownTriggerBehavior), + new PropertyMetadata(VirtualKeyModifiers.None)); + + /// + /// Gets or sets the modifier keys that must be pressed together with . + /// + public VirtualKeyModifiers Modifiers + { + get => (VirtualKeyModifiers)GetValue(ModifiersProperty); + set => SetValue(ModifiersProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty CheckModifierKeysProperty = + DependencyProperty.Register( + nameof(CheckModifierKeys), + typeof(bool), + typeof(KeyDownTriggerBehavior), + new PropertyMetadata(false)); + + /// + /// Gets or sets a value indicating whether the behavior should evaluate + /// the current modifier key state when matching the . + /// + /// When (default), the behavior ignores the state of + /// modifier keys and triggers solely based on the value, + /// preserving the original behavior. + /// + /// When , the behavior requires the modifier keys + /// specified in to match the current keyboard state + /// before triggering. + /// + public bool CheckModifierKeys + { + get => (bool)GetValue(CheckModifierKeysProperty); + set => SetValue(CheckModifierKeysProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty HandledEventsTooProperty = + DependencyProperty.Register( + nameof(HandledEventsToo), + typeof(bool), + typeof(KeyDownTriggerBehavior), + new PropertyMetadata(true)); + + /// + /// Gets or sets a value indicating whether the behavior should receive + /// events even if they were already handled. + /// + public bool HandledEventsToo + { + get => (bool)GetValue(HandledEventsTooProperty); + set => SetValue(HandledEventsTooProperty, value); + } + /// protected override void OnAttached() { - AssociatedObject.KeyDown += OnAssociatedObjectKeyDown; + _handler = OnPreviewKeyDown; + + AssociatedObject.AddHandler( + UIElement.PreviewKeyDownEvent, + _handler, + HandledEventsToo); } /// protected override void OnDetaching() { - AssociatedObject.KeyDown -= OnAssociatedObjectKeyDown; + if (_handler is not null) + { + AssociatedObject.RemoveHandler( + UIElement.PreviewKeyDownEvent, + _handler); + + _handler = null; + } } /// - /// Invokes the current actions when the is pressed. + /// Handles the event and executes the associated actions + /// when the specified and match. /// /// The source instance. /// The arguments for the event (unused). - private void OnAssociatedObjectKeyDown(object sender, KeyRoutedEventArgs keyRoutedEventArgs) + private void OnPreviewKeyDown(object sender, KeyRoutedEventArgs keyRoutedEventArgs) { - if (keyRoutedEventArgs.Key == Key) + if (keyRoutedEventArgs.Key != Key) { - keyRoutedEventArgs.Handled = true; - Interaction.ExecuteActions(sender, Actions, keyRoutedEventArgs); + return; } + + if (CheckModifierKeys && !CheckModifiers()) + { + return; + } + + keyRoutedEventArgs.Handled = true; + Interaction.ExecuteActions(sender, Actions, keyRoutedEventArgs); } -} + /// + /// Checks whether all required modifier keys specified in + /// are currently pressed. Retrieves the physical key states once and evaluates + /// them against the required modifier flags. + /// + /// if the current modifier state matches the requirements; otherwise, . + private bool CheckModifiers() + { + bool ctrl = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down); + bool shift = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down); + bool alt = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down); + + return Match(VirtualKeyModifiers.Control, ctrl) + && Match(VirtualKeyModifiers.Shift, shift) + && Match(VirtualKeyModifiers.Menu, alt); + } + + /// + /// Compares whether a specific modifier flag is required and whether the + /// corresponding key is currently pressed. + /// + /// The modifier flag to evaluate. + /// The current physical key state for that modifier. + /// if the requirement matches the key state; otherwise, . + private bool Match(VirtualKeyModifiers mod, bool isDown) + { + bool required = (Modifiers & mod) != 0; + return required == isDown; + } +}