diff --git a/src/Avalonia.Base/Input/DragEventArgs.cs b/src/Avalonia.Base/Input/DragEventArgs.cs index cf86f22c2b..d4a058e5b6 100644 --- a/src/Avalonia.Base/Input/DragEventArgs.cs +++ b/src/Avalonia.Base/Input/DragEventArgs.cs @@ -5,7 +5,7 @@ using Avalonia.Metadata; namespace Avalonia.Input { - public class DragEventArgs : RoutedEventArgs + public class DragEventArgs : RoutedEventArgs, IKeyModifiersEventArgs { private readonly Interactive _target; private readonly Point _targetLocation; diff --git a/src/Avalonia.Base/Input/FocusChangingEventArgs.cs b/src/Avalonia.Base/Input/FocusChangingEventArgs.cs index 0c392c1989..372ddf38b6 100644 --- a/src/Avalonia.Base/Input/FocusChangingEventArgs.cs +++ b/src/Avalonia.Base/Input/FocusChangingEventArgs.cs @@ -7,7 +7,7 @@ using Avalonia.Interactivity; namespace Avalonia.Input { - public class FocusChangingEventArgs : RoutedEventArgs + public class FocusChangingEventArgs : RoutedEventArgs, IKeyModifiersEventArgs { /// /// Provides data for focus changing. diff --git a/src/Avalonia.Base/Input/GotFocusEventArgs.cs b/src/Avalonia.Base/Input/GotFocusEventArgs.cs index 8d15c3f9ec..658bf5aae5 100644 --- a/src/Avalonia.Base/Input/GotFocusEventArgs.cs +++ b/src/Avalonia.Base/Input/GotFocusEventArgs.cs @@ -5,7 +5,7 @@ namespace Avalonia.Input /// /// Holds arguments for a . /// - public class GotFocusEventArgs : RoutedEventArgs + public class GotFocusEventArgs : RoutedEventArgs, IKeyModifiersEventArgs { public GotFocusEventArgs() : base(InputElement.GotFocusEvent) { diff --git a/src/Avalonia.Base/Input/IKeyModifiersEventArgs.cs b/src/Avalonia.Base/Input/IKeyModifiersEventArgs.cs new file mode 100644 index 0000000000..69770b47a3 --- /dev/null +++ b/src/Avalonia.Base/Input/IKeyModifiersEventArgs.cs @@ -0,0 +1,15 @@ +using Avalonia.Metadata; + +namespace Avalonia.Input; + +/// +/// Represents an event associated with a set of . +/// +[NotClientImplementable] +public interface IKeyModifiersEventArgs +{ + /// + /// Gets the key modifiers associated with this event. + /// + KeyModifiers KeyModifiers { get; } +} diff --git a/src/Avalonia.Base/Input/KeyEventArgs.cs b/src/Avalonia.Base/Input/KeyEventArgs.cs index 864aa02617..0fed020a91 100644 --- a/src/Avalonia.Base/Input/KeyEventArgs.cs +++ b/src/Avalonia.Base/Input/KeyEventArgs.cs @@ -5,7 +5,7 @@ namespace Avalonia.Input; /// /// Provides information specific to a keyboard event. /// -public class KeyEventArgs : RoutedEventArgs +public class KeyEventArgs : RoutedEventArgs, IKeyModifiersEventArgs { /// /// @@ -33,9 +33,6 @@ public class KeyEventArgs : RoutedEventArgs /// public Key Key { get; init; } - /// - /// Gets the key modifiers for the associated event. - /// public KeyModifiers KeyModifiers { get; init; } /// diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index 7682b0fb22..f4bf785c56 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -7,7 +7,7 @@ using Avalonia.VisualTree; namespace Avalonia.Input { - public class PointerEventArgs : RoutedEventArgs + public class PointerEventArgs : RoutedEventArgs, IKeyModifiersEventArgs { private readonly Visual? _rootVisual; private readonly Point _rootVisualPosition; diff --git a/src/Avalonia.Base/Input/TappedEventArgs.cs b/src/Avalonia.Base/Input/TappedEventArgs.cs index 663207a104..eaffa1d8bc 100644 --- a/src/Avalonia.Base/Input/TappedEventArgs.cs +++ b/src/Avalonia.Base/Input/TappedEventArgs.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Input { - public class TappedEventArgs : RoutedEventArgs + public class TappedEventArgs : RoutedEventArgs, IKeyModifiersEventArgs { private readonly PointerEventArgs lastPointerEventArgs; diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index dbf49f0db5..481eb7f108 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; using Avalonia.Data; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Metadata; @@ -270,12 +271,6 @@ namespace Avalonia.Controls SetCurrentValue(IsDropDownOpenProperty, true); e.Handled = true; } - else if (IsDropDownOpen && (e.Key == Key.Enter || e.Key == Key.Space)) - { - SelectFocusedItem(); - SetCurrentValue(IsDropDownOpenProperty, false); - e.Handled = true; - } else if (IsDropDownOpen && e.Key == Key.Tab) { SetCurrentValue(IsDropDownOpenProperty, false); @@ -366,15 +361,7 @@ namespace Avalonia.Controls if (!e.Handled && e.Source is Visual source) { - if (_popup?.IsInsidePopup(source) == true) - { - if (UpdateSelectionFromEventSource(e.Source)) - { - _popup?.Close(); - e.Handled = true; - } - } - else if (PseudoClasses.Contains(pcPressed)) + if (_popup?.IsInsidePopup(source) != true && PseudoClasses.Contains(pcPressed)) { SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen); e.Handled = true; @@ -385,6 +372,22 @@ namespace Avalonia.Controls base.OnPointerReleased(e); } + public override bool UpdateSelectionFromEvent(Control container, RoutedEventArgs eventArgs) + { + if (base.UpdateSelectionFromEvent(container, eventArgs)) + { + _popup?.Close(); + return true; + } + + return false; + } + + protected override bool ShouldTriggerSelection(Visual selectable, PointerEventArgs eventArgs) => + ItemSelectionEventTriggers.IsPointerEventWithinBounds(selectable, eventArgs) && + eventArgs is { Properties.PointerUpdateKind: PointerUpdateKind.LeftButtonReleased or PointerUpdateKind.RightButtonReleased } && + eventArgs.RoutedEvent == PointerReleasedEvent; + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { @@ -602,18 +605,6 @@ namespace Avalonia.Controls SetCurrentValue(TextProperty, GetItemTextValue(item)); } - private void SelectFocusedItem() - { - foreach (var dropdownItem in GetRealizedContainers()) - { - if (dropdownItem.IsFocused) - { - SelectedIndex = IndexFromContainer(dropdownItem); - break; - } - } - } - private bool SelectNext() => MoveSelection(SelectedIndex, 1, WrapSelection); private bool SelectPrevious() => MoveSelection(SelectedIndex, -1, WrapSelection); diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 204847a37e..2f71bfb0bb 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -146,14 +146,6 @@ namespace Avalonia.Controls Selection.SelectAll(); e.Handled = true; } - else if (e.Key == Key.Space || e.Key == Key.Enter) - { - UpdateSelectionFromEventSource( - e.Source, - true, - e.KeyModifiers.HasFlag(KeyModifiers.Shift), - ctrl); - } base.OnKeyDown(e); } @@ -163,19 +155,5 @@ namespace Avalonia.Controls base.OnApplyTemplate(e); Scroll = e.NameScope.Find("PART_ScrollViewer"); } - - internal bool UpdateSelectionFromPointerEvent(Control source, PointerEventArgs e) - { - // TODO: use TopLevel.PlatformSettings here, but first need to update our tests to use TopLevels. - var hotkeys = Application.Current!.PlatformSettings?.HotkeyConfiguration; - var toggle = hotkeys is not null && e.KeyModifiers.HasAllFlags(hotkeys.CommandModifiers); - - return UpdateSelectionFromEventSource( - source, - true, - e.KeyModifiers.HasAllFlags(KeyModifiers.Shift), - toggle, - e.GetCurrentPoint(source).Properties.IsRightButtonPressed); - } } } diff --git a/src/Avalonia.Controls/ListBoxItem.cs b/src/Avalonia.Controls/ListBoxItem.cs index da298c219a..274a224a17 100644 --- a/src/Avalonia.Controls/ListBoxItem.cs +++ b/src/Avalonia.Controls/ListBoxItem.cs @@ -4,7 +4,7 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Input; -using Avalonia.Platform; +using Avalonia.Interactivity; namespace Avalonia.Controls { @@ -20,9 +20,6 @@ namespace Avalonia.Controls public static readonly StyledProperty IsSelectedProperty = SelectingItemsControl.IsSelectedProperty.AddOwner(); - private static readonly Point s_invalidPoint = new Point(double.NaN, double.NaN); - private Point _pointerDownPoint = s_invalidPoint; - /// /// Initializes static members of the class. /// @@ -51,76 +48,21 @@ namespace Avalonia.Controls protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); - - _pointerDownPoint = s_invalidPoint; - - if (e.Handled) - return; - - if (!e.Handled && ItemsControl.ItemsControlFromItemContainer(this) is ListBox owner) - { - var p = e.GetCurrentPoint(this); - - if (p.Properties.PointerUpdateKind is PointerUpdateKind.LeftButtonPressed or - PointerUpdateKind.RightButtonPressed) - { - if (p.Pointer.Type == PointerType.Mouse - || (p.Pointer.Type == PointerType.Pen && p.Properties.IsRightButtonPressed)) - { - // If the pressed point comes from a mouse or right-click pen, perform the selection immediately. - // In case of pen, only right-click is accepted, as left click (a tip touch) is used for scrolling. - e.Handled = owner.UpdateSelectionFromPointerEvent(this, e); - } - else - { - // Otherwise perform the selection when the pointer is released as to not - // interfere with gestures. - _pointerDownPoint = p.Position; - - // Ideally we'd set handled here, but that would prevent the scroll gesture - // recognizer from working. - ////e.Handled = true; - } - } - } + UpdateSelectionFromEvent(e); } protected override void OnPointerReleased(PointerReleasedEventArgs e) { base.OnPointerReleased(e); + UpdateSelectionFromEvent(e); + } - if (!e.Handled && - !double.IsNaN(_pointerDownPoint.X) && - e.InitialPressMouseButton is MouseButton.Left or MouseButton.Right) - { - var point = e.GetCurrentPoint(this); - var settings = TopLevel.GetTopLevel(e.Source as Visual)?.PlatformSettings; - var tapSize = settings?.GetTapSize(point.Pointer.Type) ?? new Size(4, 4); - var tapRect = new Rect(_pointerDownPoint, new Size()) - .Inflate(new Thickness(tapSize.Width, tapSize.Height)); - - if (new Rect(Bounds.Size).ContainsExclusive(point.Position) && - tapRect.ContainsExclusive(point.Position) && - ItemsControl.ItemsControlFromItemContainer(this) is ListBox owner) - { - if (owner.UpdateSelectionFromPointerEvent(this, e)) - { - // As we only update selection from touch/pen on pointer release, we need to raise - // the pointer event on the owner to trigger a commit. - if (e.Pointer.Type != PointerType.Mouse) - { - var sourceBackup = e.Source; - owner.RaiseEvent(e); - e.Source = sourceBackup; - } - - e.Handled = true; - } - } - } - - _pointerDownPoint = s_invalidPoint; + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + UpdateSelectionFromEvent(e); } + protected bool UpdateSelectionFromEvent(RoutedEventArgs e) => SelectingItemsControl.ItemsControlFromItemContainer(this)?.UpdateSelectionFromEvent(this, e) ?? false; } } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 54bae5b891..dcdcec069a 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -484,7 +484,7 @@ namespace Avalonia.Controls protected override void OnGotFocus(GotFocusEventArgs e) { base.OnGotFocus(e); - e.Handled = UpdateSelectionFromEventSource(e.Source, true); + ItemsControlFromItemContainer(this)?.UpdateSelectionFromEvent(this, e); } /// diff --git a/src/Avalonia.Controls/Primitives/ItemSelectionEventTriggers.cs b/src/Avalonia.Controls/Primitives/ItemSelectionEventTriggers.cs new file mode 100644 index 0000000000..b7a41678db --- /dev/null +++ b/src/Avalonia.Controls/Primitives/ItemSelectionEventTriggers.cs @@ -0,0 +1,82 @@ +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Interactivity; + +namespace Avalonia.Controls.Primitives; + +/// +/// Defines standard logic for selecting items via user input. Behaviour differs between input devices. +/// +public static class ItemSelectionEventTriggers +{ + /// + /// Analyses an input event received by a selectable element, and determines whether the action should trigger selection on press, on release, or not at all. + /// + /// The selectable element which is processing the event. + /// The event to analyse. + public static bool ShouldTriggerSelection(Visual selectable, PointerEventArgs eventArgs) + { + if (!IsPointerEventWithinBounds(selectable, eventArgs)) + { + return false; // don't select if the pointer has moved away from the element since being pressed + } + + return eventArgs switch + { + // Only select for left/right button events + { + Properties.PointerUpdateKind: not (PointerUpdateKind.LeftButtonPressed or PointerUpdateKind.RightButtonPressed or + PointerUpdateKind.LeftButtonReleased or PointerUpdateKind.RightButtonReleased) + } => false, + + // Select on mouse press, unless the mouse can generate gestures + { Pointer.Type: PointerType.Mouse } => eventArgs.RoutedEvent == (Gestures.GetIsHoldWithMouseEnabled(selectable) ? + InputElement.PointerReleasedEvent : (RoutedEvent)InputElement.PointerPressedEvent), + + // Pen "right clicks" are used for context menus, and gestures are only processed for primary input + { Pointer.Type: PointerType.Pen, Properties.PointerUpdateKind: PointerUpdateKind.RightButtonPressed or PointerUpdateKind.RightButtonReleased } => + eventArgs.RoutedEvent == InputElement.PointerPressedEvent, + + // For all other pen input, select on release + { Pointer.Type: PointerType.Pen } => eventArgs.RoutedEvent == InputElement.PointerReleasedEvent, + + // Select on touch release + { Pointer.Type: PointerType.Touch } => eventArgs.RoutedEvent == InputElement.PointerReleasedEvent, + + // Don't select in any other case + _ => false, + }; + } + + internal static bool IsPointerEventWithinBounds(Visual selectable, PointerEventArgs eventArgs) => + new Rect(selectable.Bounds.Size).Contains(eventArgs.GetPosition(selectable)); + + /// + public static bool ShouldTriggerSelection(Visual selectable, KeyEventArgs eventArgs) + { + // Only accept space/enter key presses directly from the selectable, otherwise key input can become unpredictable + return eventArgs.Source == selectable && eventArgs.Key is Key.Space or Key.Enter ? eventArgs.RoutedEvent == InputElement.KeyDownEvent : false; + } + + /// + /// Analyses an input event received by a selectable element, and determines whether the action should trigger range selection. + /// + /// The selectable element which is processing the event. + /// The event to analyse. + /// + public static bool HasRangeSelectionModifier(Visual selectable, RoutedEventArgs eventArgs) => HasModifiers(eventArgs, Hotkeys(selectable)?.SelectionModifiers); + + /// + /// Analyses an input event received by a selectable element, and determines whether the action should trigger toggle selection. + /// + /// The selectable element which is processing the event. + /// The event to analyse. + /// + public static bool HasToggleSelectionModifier(Visual selectable, RoutedEventArgs eventArgs) => HasModifiers(eventArgs, Hotkeys(selectable)?.CommandModifiers); + + private static PlatformHotkeyConfiguration? Hotkeys(Visual element) => + (TopLevel.GetTopLevel(element)?.PlatformSettings ?? Application.Current?.PlatformSettings)?.HotkeyConfiguration; + + private static bool HasModifiers(RoutedEventArgs eventArgs, KeyModifiers? modifiers) => + modifiers != null && eventArgs is IKeyModifiersEventArgs { KeyModifiers: { } eventModifiers } && eventModifiers.HasAllFlags(modifiers.Value); +} diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index b1b7ae7f4e..ce82d08671 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Selection; using Avalonia.Controls.Utils; using Avalonia.Data; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Metadata; using Avalonia.Threading; @@ -115,7 +116,7 @@ namespace Avalonia.Controls.Primitives /// public static readonly StyledProperty IsTextSearchEnabledProperty = AvaloniaProperty.Register(nameof(IsTextSearchEnabled), false); - + /// /// Event that should be raised by containers when their selection state changes to notify /// the parent that their selection state has changed. @@ -445,6 +446,9 @@ namespace Avalonia.Controls.Primitives return null; } + /// + public new static SelectingItemsControl? ItemsControlFromItemContainer(Control container) => ItemsControl.ItemsControlFromItemContainer(container) as SelectingItemsControl; + private protected override void OnItemsViewCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { base.OnItemsViewCollectionChanged(sender!, e); @@ -594,7 +598,7 @@ namespace Avalonia.Controls.Primitives { SelectedIndex = newIndex; } - + StartTextSearchTimer(); e.Handled = true; @@ -813,6 +817,7 @@ namespace Avalonia.Controls.Primitives /// Whether the toggle modifier is enabled (i.e. ctrl key). /// Whether the event is a right-click. /// Wheter the event is a focus event + [Obsolete($"Call {nameof(UpdateSelectionFromEvent)} instead.")] protected void UpdateSelection( Control container, bool select = true, @@ -829,10 +834,7 @@ namespace Avalonia.Controls.Primitives } } - /// - /// Updates the selection based on an event that may have originated in a container that - /// belongs to the control. - /// + /// /// The control that raised the event. /// Whether the container should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). @@ -843,6 +845,7 @@ namespace Avalonia.Controls.Primitives /// True if the event originated from a container that belongs to the control; otherwise /// false. /// + [Obsolete($"Call {nameof(UpdateSelectionFromEvent)} instead.")] protected bool UpdateSelectionFromEventSource( object? eventSource, bool select = true, @@ -861,6 +864,52 @@ namespace Avalonia.Controls.Primitives return false; } + + /// + /// + protected virtual bool ShouldTriggerSelection(Visual selectable, PointerEventArgs eventArgs) => ItemSelectionEventTriggers.ShouldTriggerSelection(selectable, eventArgs); + + /// + protected virtual bool ShouldTriggerSelection(Visual selectable, KeyEventArgs eventArgs) => ItemSelectionEventTriggers.ShouldTriggerSelection(selectable, eventArgs); + + /// + /// Updates the selection based on an event that may have originated in a container that + /// belongs to the control. + /// + /// True if the event was accepted and handled, otherwise false. + /// + /// + public virtual bool UpdateSelectionFromEvent(Control container, RoutedEventArgs eventArgs) + { + if (eventArgs.Handled) + { + return false; + } + + var containerIndex = IndexFromContainer(container); + if (containerIndex == -1) + { + return false; + } + + switch (eventArgs) + { + case PointerEventArgs pointerEvent when ShouldTriggerSelection(container, pointerEvent): + case KeyEventArgs keyEvent when ShouldTriggerSelection(container, keyEvent): + case GotFocusEventArgs: + UpdateSelection(containerIndex, true, + ItemSelectionEventTriggers.HasRangeSelectionModifier(container, eventArgs), + ItemSelectionEventTriggers.HasToggleSelectionModifier(container, eventArgs), + eventArgs is PointerEventArgs { Properties.IsRightButtonPressed: true }, + eventArgs is GotFocusEventArgs); + + eventArgs.Handled = true; + return true; + + default: + return false; + } + } private ISelectionModel GetOrCreateSelectionModel() { diff --git a/src/Avalonia.Controls/Primitives/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs index ac64b827d5..29cf48ce9e 100644 --- a/src/Avalonia.Controls/Primitives/TabStrip.cs +++ b/src/Avalonia.Controls/Primitives/TabStrip.cs @@ -1,5 +1,6 @@ using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Layout; namespace Avalonia.Controls.Primitives @@ -26,31 +27,17 @@ namespace Avalonia.Controls.Primitives return NeedsContainer(item, out recycleKey); } - /// - protected override void OnGotFocus(GotFocusEventArgs e) - { - base.OnGotFocus(e); - - if (e.NavigationMethod == NavigationMethod.Directional) - { - e.Handled = UpdateSelectionFromEventSource(e.Source); - } - } + protected override bool ShouldTriggerSelection(Visual selectable, PointerEventArgs e) => + e.Properties.PointerUpdateKind is PointerUpdateKind.LeftButtonPressed or PointerUpdateKind.LeftButtonReleased && base.ShouldTriggerSelection(selectable, e); - /// - protected override void OnPointerPressed(PointerPressedEventArgs e) + public override bool UpdateSelectionFromEvent(Control container, RoutedEventArgs eventArgs) { - base.OnPointerPressed(e); - - if (e.Source is Visual source) + if (eventArgs is GotFocusEventArgs { NavigationMethod: not NavigationMethod.Directional }) { - var point = e.GetCurrentPoint(source); - - if (point.Properties.IsLeftButtonPressed) - { - e.Handled = UpdateSelectionFromEventSource(e.Source); - } + return false; } + + return base.UpdateSelectionFromEvent(container, eventArgs); } } } diff --git a/src/Avalonia.Controls/Primitives/TabStripItem.cs b/src/Avalonia.Controls/Primitives/TabStripItem.cs index fb2016b485..0053255613 100644 --- a/src/Avalonia.Controls/Primitives/TabStripItem.cs +++ b/src/Avalonia.Controls/Primitives/TabStripItem.cs @@ -1,3 +1,5 @@ +using Avalonia.Input; + namespace Avalonia.Controls.Primitives { /// @@ -5,5 +7,10 @@ namespace Avalonia.Controls.Primitives /// public class TabStripItem : ListBoxItem { + protected override void OnGotFocus(GotFocusEventArgs e) + { + base.OnGotFocus(e); + UpdateSelectionFromEvent(e); + } } } diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 6cc984f590..396f82a622 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -11,6 +11,7 @@ using Avalonia.VisualTree; using Avalonia.Automation; using Avalonia.Controls.Metadata; using Avalonia.Reactive; +using Avalonia.Interactivity; namespace Avalonia.Controls { @@ -193,6 +194,19 @@ namespace Avalonia.Controls UpdateSelectedContent(); } + protected override bool ShouldTriggerSelection(Visual selectable, PointerEventArgs eventArgs) => + eventArgs.Properties.PointerUpdateKind is PointerUpdateKind.LeftButtonPressed or PointerUpdateKind.LeftButtonReleased && base.ShouldTriggerSelection(selectable, eventArgs); + + public override bool UpdateSelectionFromEvent(Control container, RoutedEventArgs eventArgs) + { + if (eventArgs is GotFocusEventArgs { NavigationMethod: not NavigationMethod.Directional }) + { + return false; + } + + return base.UpdateSelectionFromEvent(container, eventArgs); + } + private void UpdateSelectedContent(Control? container = null) { _selectedItemSubscriptions?.Dispose(); @@ -262,42 +276,6 @@ namespace Avalonia.Controls } } - /// - protected override void OnGotFocus(GotFocusEventArgs e) - { - base.OnGotFocus(e); - - if (e.NavigationMethod == NavigationMethod.Directional && e.Source is TabItem) - { - e.Handled = UpdateSelectionFromEventSource(e.Source); - } - } - - /// - protected override void OnPointerPressed(PointerPressedEventArgs e) - { - base.OnPointerPressed(e); - - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && e.Pointer.Type == PointerType.Mouse) - { - e.Handled = UpdateSelectionFromEventSource(e.Source); - } - } - - protected override void OnPointerReleased(PointerReleasedEventArgs e) - { - if (e.InitialPressMouseButton == MouseButton.Left && e.Pointer.Type != PointerType.Mouse) - { - var container = GetContainerFromEventSource(e.Source); - if (container != null - && container.GetVisualsAt(e.GetPosition(container)) - .Any(c => container == c || container.IsVisualAncestorOf(c))) - { - e.Handled = UpdateSelectionFromEventSource(e.Source); - } - } - } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 1b2ab6505b..78785876ad 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -71,7 +71,6 @@ namespace Avalonia.Controls protected override AutomationPeer OnCreateAutomationPeer() => new ListItemAutomationPeer(this); - [Obsolete("Owner manages its children properties by itself")] protected void SubscribeToOwnerProperties(AvaloniaObject owner) { @@ -79,13 +78,33 @@ namespace Avalonia.Controls private static void OnAccessKeyPressed(TabItem tabItem, AccessKeyPressedEventArgs e) { - if (e.Handled || (e.Target != null && tabItem.IsSelected)) + if (e.Handled || (e.Target != null && tabItem.IsSelected)) return; - + e.Target = tabItem; e.Handled = true; } + protected override void OnGotFocus(GotFocusEventArgs e) + { + base.OnGotFocus(e); + UpdateSelectionFromEvent(e); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + UpdateSelectionFromEvent(e); + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + UpdateSelectionFromEvent(e); + } + + protected bool UpdateSelectionFromEvent(RoutedEventArgs e) => SelectingItemsControl.ItemsControlFromItemContainer(this)?.UpdateSelectionFromEvent(this, e) ?? false; + private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj) { if (Header == null) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 98127545f4..809e200e8c 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -11,6 +11,7 @@ using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Threading; @@ -662,25 +663,36 @@ namespace Avalonia.Controls return result; } - /// - protected override void OnPointerPressed(PointerPressedEventArgs e) + /// + /// + protected virtual bool ShouldTriggerSelection(Visual selectable, PointerEventArgs eventArgs) => ItemSelectionEventTriggers.ShouldTriggerSelection(selectable, eventArgs); + + /// + protected virtual bool ShouldTriggerSelection(Visual selectable, KeyEventArgs eventArgs) => ItemSelectionEventTriggers.ShouldTriggerSelection(selectable, eventArgs); + + /// + /// + public virtual bool UpdateSelectionFromEvent(Control container, RoutedEventArgs eventArgs) { - base.OnPointerPressed(e); + if (eventArgs.Handled) + { + return false; + } - if (e.Source is Visual source) + switch (eventArgs) { - var point = e.GetCurrentPoint(source); + case PointerEventArgs pointerEvent when ShouldTriggerSelection(container, pointerEvent): + case KeyEventArgs keyEvent when ShouldTriggerSelection(container, keyEvent): + UpdateSelectionFromContainer(container, true, + ItemSelectionEventTriggers.HasRangeSelectionModifier(container, eventArgs), + ItemSelectionEventTriggers.HasToggleSelectionModifier(container, eventArgs), + eventArgs is PointerEventArgs { Properties.IsRightButtonPressed: true }); - if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed) - { - var keymap = Application.Current!.PlatformSettings!.HotkeyConfiguration; - e.Handled = UpdateSelectionFromEventSource( - e.Source, - true, - e.KeyModifiers.HasAllFlags(KeyModifiers.Shift), - e.KeyModifiers.HasAllFlags(keymap.CommandModifiers), - point.Properties.IsRightButtonPressed); - } + eventArgs.Handled = true; + return true; + + default: + return false; } } diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index e38709c5f5..ec1e943da0 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -229,6 +229,10 @@ namespace Avalonia.Controls { e.Handled = handler(this); } + else + { + TreeViewOwner?.UpdateSelectionFromEvent(this, e); + } // NOTE: these local functions do not use the TreeView.Expand/CollapseSubtree // function because we want to know if any items were in fact expanded to set the @@ -304,6 +308,18 @@ namespace Avalonia.Controls // Don't call base.OnKeyDown - let events bubble up to containing TreeView. } + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + TreeViewOwner?.UpdateSelectionFromEvent(this, e); + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + TreeViewOwner?.UpdateSelectionFromEvent(this, e); + } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { if (_headerPresenter is InputElement previousInputMethod) diff --git a/tests/Avalonia.UnitTests/MouseTestHelper.cs b/tests/Avalonia.UnitTests/MouseTestHelper.cs index c963efedd0..0749954b23 100644 --- a/tests/Avalonia.UnitTests/MouseTestHelper.cs +++ b/tests/Avalonia.UnitTests/MouseTestHelper.cs @@ -38,14 +38,14 @@ namespace Avalonia.UnitTests private MouseButton _pressedButton; - public void Down(Interactive target, MouseButton mouseButton = MouseButton.Left, Point position = default, + public void Down(Interactive target, MouseButton mouseButton = MouseButton.Left, Point? position = null, KeyModifiers modifiers = default, int clickCount = 1) { Down(target, target, mouseButton, position, modifiers, clickCount); } public void Down(Interactive target, Interactive source, MouseButton mouseButton = MouseButton.Left, - Point position = default, KeyModifiers modifiers = default, int clickCount = 1) + Point? position = null, KeyModifiers modifiers = default, int clickCount = 1) { _pressedButtons |= Convert(mouseButton); var props = new PointerPointProperties((RawInputModifiers)_pressedButtons, @@ -54,12 +54,12 @@ namespace Avalonia.UnitTests : mouseButton == MouseButton.Right ? PointerUpdateKind.RightButtonPressed : PointerUpdateKind.Other ); if (ButtonCount(props) > 1) - Move(target, source, position); + Move(target, source, position ?? default); else { _pressedButton = mouseButton; _pointer.Capture((IInputElement)target); - source.RaiseEvent(new PointerPressedEventArgs(source, _pointer, GetRoot(target), position, Timestamp(), props, + source.RaiseEvent(new PointerPressedEventArgs(source, _pointer, GetRoot(target), position ?? MidpointRelativeToRoot(target), Timestamp(), props, modifiers, clickCount)); } } @@ -72,12 +72,12 @@ namespace Avalonia.UnitTests Timestamp(), new PointerPointProperties((RawInputModifiers)_pressedButtons, PointerUpdateKind.Other), modifiers)); } - public void Up(Interactive target, MouseButton mouseButton = MouseButton.Left, Point position = default, + public void Up(Interactive target, MouseButton mouseButton = MouseButton.Left, Point? position = null, KeyModifiers modifiers = default) => Up(target, target, mouseButton, position, modifiers); public void Up(Interactive target, Interactive source, MouseButton mouseButton = MouseButton.Left, - Point position = default, KeyModifiers modifiers = default) + Point? position = null, KeyModifiers modifiers = default) { var conv = Convert(mouseButton); _pressedButtons = (_pressedButtons | conv) ^ conv; @@ -88,32 +88,32 @@ namespace Avalonia.UnitTests ); if (ButtonCount(props) == 0) { - target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, GetRoot(target), position, + target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, GetRoot(target), position ?? MidpointRelativeToRoot(target), Timestamp(), props, modifiers, _pressedButton)); _pointer.Capture(null); } else - Move(target, source, position); + Move(target, source, position ?? default); } - public void Click(Interactive target, MouseButton button = MouseButton.Left, Point position = default, + public void Click(Interactive target, MouseButton button = MouseButton.Left, Point? position = null, KeyModifiers modifiers = default) => Click(target, target, button, position, modifiers); public void Click(Interactive target, Interactive source, MouseButton button = MouseButton.Left, - Point position = default, KeyModifiers modifiers = default) + Point? position = null, KeyModifiers modifiers = default) { Down(target, source, button, position, modifiers); var captured = (_pointer.Captured as Interactive) ?? source; Up(captured, captured, button, position, modifiers); } - public void DoubleClick(Interactive target, MouseButton button = MouseButton.Left, Point position = default, + public void DoubleClick(Interactive target, MouseButton button = MouseButton.Left, Point? position = null, KeyModifiers modifiers = default) => DoubleClick(target, target, button, position, modifiers); public void DoubleClick(Interactive target, Interactive source, MouseButton button = MouseButton.Left, - Point position = default, KeyModifiers modifiers = default) + Point? position = null, KeyModifiers modifiers = default) { Down(target, source, button, position, modifiers, clickCount: 1); var captured = (_pointer.Captured as Interactive) ?? source; @@ -137,5 +137,11 @@ namespace Avalonia.UnitTests { return ((source as Visual)?.GetVisualRoot() as Visual) ?? (Visual)source; } + + private Point MidpointRelativeToRoot(Interactive element) + { + var root = GetRoot(element); + return element.TranslatePoint(new(element.Bounds.Width / 2, element.Bounds.Height / 2), root).Value; + } } }