Browse Source

Unify selection event handling for all `SelectingItemsControl` types plus `TreeView` (#19203)

* Unify selection event handling for all SelectingItemsControl types plus TreeView
- Controls can decide whether to select on press/release, or introduce their own logic
- Container types handle events and can decide whether to forward them on to their owner
- Corrected various cases where controls checked whether a button was held when the event occurred, rather than whether it triggered the event
- Replaced various hardcoded modifier key checks with uses of PlatformHotkeyConfiguration
- ListBox no longer cares if you swipe before releasing touch (unless that triggers a gesture)
- TreeViewItem is now selected on touch/pen release

* API change requests

* Review comments
pull/20295/head
Tom Edwards 2 months ago
committed by GitHub
parent
commit
b7cb9e5770
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      src/Avalonia.Base/Input/DragEventArgs.cs
  2. 2
      src/Avalonia.Base/Input/FocusChangingEventArgs.cs
  3. 2
      src/Avalonia.Base/Input/GotFocusEventArgs.cs
  4. 15
      src/Avalonia.Base/Input/IKeyModifiersEventArgs.cs
  5. 5
      src/Avalonia.Base/Input/KeyEventArgs.cs
  6. 2
      src/Avalonia.Base/Input/PointerEventArgs.cs
  7. 2
      src/Avalonia.Base/Input/TappedEventArgs.cs
  8. 45
      src/Avalonia.Controls/ComboBox.cs
  9. 22
      src/Avalonia.Controls/ListBox.cs
  10. 76
      src/Avalonia.Controls/ListBoxItem.cs
  11. 2
      src/Avalonia.Controls/MenuItem.cs
  12. 82
      src/Avalonia.Controls/Primitives/ItemSelectionEventTriggers.cs
  13. 61
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  14. 29
      src/Avalonia.Controls/Primitives/TabStrip.cs
  15. 7
      src/Avalonia.Controls/Primitives/TabStripItem.cs
  16. 50
      src/Avalonia.Controls/TabControl.cs
  17. 25
      src/Avalonia.Controls/TabItem.cs
  18. 42
      src/Avalonia.Controls/TreeView.cs
  19. 16
      src/Avalonia.Controls/TreeViewItem.cs
  20. 30
      tests/Avalonia.UnitTests/MouseTestHelper.cs

2
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;

2
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
{
/// <summary>
/// Provides data for focus changing.

2
src/Avalonia.Base/Input/GotFocusEventArgs.cs

@ -5,7 +5,7 @@ namespace Avalonia.Input
/// <summary>
/// Holds arguments for a <see cref="InputElement.GotFocusEvent"/>.
/// </summary>
public class GotFocusEventArgs : RoutedEventArgs
public class GotFocusEventArgs : RoutedEventArgs, IKeyModifiersEventArgs
{
public GotFocusEventArgs() : base(InputElement.GotFocusEvent)
{

15
src/Avalonia.Base/Input/IKeyModifiersEventArgs.cs

@ -0,0 +1,15 @@
using Avalonia.Metadata;
namespace Avalonia.Input;
/// <summary>
/// Represents an event associated with a set of <see cref="Input.KeyModifiers"/>.
/// </summary>
[NotClientImplementable]
public interface IKeyModifiersEventArgs
{
/// <summary>
/// Gets the key modifiers associated with this event.
/// </summary>
KeyModifiers KeyModifiers { get; }
}

5
src/Avalonia.Base/Input/KeyEventArgs.cs

@ -5,7 +5,7 @@ namespace Avalonia.Input;
/// <summary>
/// Provides information specific to a keyboard event.
/// </summary>
public class KeyEventArgs : RoutedEventArgs
public class KeyEventArgs : RoutedEventArgs, IKeyModifiersEventArgs
{
/// <summary>
/// <para>
@ -33,9 +33,6 @@ public class KeyEventArgs : RoutedEventArgs
/// <seealso cref="KeySymbol"/>
public Key Key { get; init; }
/// <summary>
/// Gets the key modifiers for the associated event.
/// </summary>
public KeyModifiers KeyModifiers { get; init; }
/// <summary>

2
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;

2
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;

45
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;
/// <inheritdoc/>
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);

22
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<IScrollable>("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);
}
}
}

76
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<bool> IsSelectedProperty =
SelectingItemsControl.IsSelectedProperty.AddOwner<ListBoxItem>();
private static readonly Point s_invalidPoint = new Point(double.NaN, double.NaN);
private Point _pointerDownPoint = s_invalidPoint;
/// <summary>
/// Initializes static members of the <see cref="ListBoxItem"/> class.
/// </summary>
@ -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;
}
}

2
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);
}
/// <inheritdoc/>

82
src/Avalonia.Controls/Primitives/ItemSelectionEventTriggers.cs

@ -0,0 +1,82 @@
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
namespace Avalonia.Controls.Primitives;
/// <summary>
/// Defines standard logic for selecting items via user input. Behaviour differs between input devices.
/// </summary>
public static class ItemSelectionEventTriggers
{
/// <summary>
/// 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.
/// </summary>
/// <param name="selectable">The selectable element which is processing the event.</param>
/// <param name="eventArgs">The event to analyse.</param>
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));
/// <inheritdoc cref="ShouldTriggerSelection(Visual, PointerEventArgs)"/>
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;
}
/// <summary>
/// Analyses an input event received by a selectable element, and determines whether the action should trigger range selection.
/// </summary>
/// <param name="selectable">The selectable element which is processing the event.</param>
/// <param name="eventArgs">The event to analyse.</param>
/// <seealso cref="PlatformHotkeyConfiguration.SelectionModifiers"/>
public static bool HasRangeSelectionModifier(Visual selectable, RoutedEventArgs eventArgs) => HasModifiers(eventArgs, Hotkeys(selectable)?.SelectionModifiers);
/// <summary>
/// Analyses an input event received by a selectable element, and determines whether the action should trigger toggle selection.
/// </summary>
/// <param name="selectable">The selectable element which is processing the event.</param>
/// <param name="eventArgs">The event to analyse.</param>
/// <seealso cref="PlatformHotkeyConfiguration.CommandModifiers"/>
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);
}

61
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
/// </summary>
public static readonly StyledProperty<bool> IsTextSearchEnabledProperty =
AvaloniaProperty.Register<SelectingItemsControl, bool>(nameof(IsTextSearchEnabled), false);
/// <summary>
/// Event that should be raised by containers when their selection state changes to notify
/// the parent <see cref="SelectingItemsControl"/> that their selection state has changed.
@ -445,6 +446,9 @@ namespace Avalonia.Controls.Primitives
return null;
}
/// <inheritdoc cref="ItemsControl.ItemsControlFromItemContainer(Control)"/>
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
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <param name="rightButton">Whether the event is a right-click.</param>
/// <param name="fromFocus">Wheter the event is a focus event</param>
[Obsolete($"Call {nameof(UpdateSelectionFromEvent)} instead.")]
protected void UpdateSelection(
Control container,
bool select = true,
@ -829,10 +834,7 @@ namespace Avalonia.Controls.Primitives
}
}
/// <summary>
/// Updates the selection based on an event that may have originated in a container that
/// belongs to the control.
/// </summary>
/// <inheritdoc cref="UpdateSelectionFromEvent"/>
/// <param name="eventSource">The control that raised the event.</param>
/// <param name="select">Whether the container should be selected or unselected.</param>
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
@ -843,6 +845,7 @@ namespace Avalonia.Controls.Primitives
/// True if the event originated from a container that belongs to the control; otherwise
/// false.
/// </returns>
[Obsolete($"Call {nameof(UpdateSelectionFromEvent)} instead.")]
protected bool UpdateSelectionFromEventSource(
object? eventSource,
bool select = true,
@ -861,6 +864,52 @@ namespace Avalonia.Controls.Primitives
return false;
}
/// <inheritdoc cref="ItemSelectionEventTriggers.ShouldTriggerSelection(Visual, PointerEventArgs)"/>
/// <seealso cref="UpdateSelectionFromEvent"/>
protected virtual bool ShouldTriggerSelection(Visual selectable, PointerEventArgs eventArgs) => ItemSelectionEventTriggers.ShouldTriggerSelection(selectable, eventArgs);
/// <inheritdoc cref="ItemSelectionEventTriggers.ShouldTriggerSelection(Visual, KeyEventArgs)"/>
protected virtual bool ShouldTriggerSelection(Visual selectable, KeyEventArgs eventArgs) => ItemSelectionEventTriggers.ShouldTriggerSelection(selectable, eventArgs);
/// <summary>
/// Updates the selection based on an event that may have originated in a container that
/// belongs to the control.
/// </summary>
/// <returns>True if the event was accepted and handled, otherwise false.</returns>
/// <seealso cref="ShouldTriggerSelection(Visual, PointerEventArgs)"/>
/// <seealso cref="ShouldTriggerSelection(Visual, KeyEventArgs)"/>
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()
{

29
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<TabStripItem>(item, out recycleKey);
}
/// <inheritdoc/>
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);
/// <inheritdoc/>
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);
}
}
}

7
src/Avalonia.Controls/Primitives/TabStripItem.cs

@ -1,3 +1,5 @@
using Avalonia.Input;
namespace Avalonia.Controls.Primitives
{
/// <summary>
@ -5,5 +7,10 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public class TabStripItem : ListBoxItem
{
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
UpdateSelectionFromEvent(e);
}
}
}

50
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
}
}
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
if (e.NavigationMethod == NavigationMethod.Directional && e.Source is TabItem)
{
e.Handled = UpdateSelectionFromEventSource(e.Source);
}
}
/// <inheritdoc/>
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);

25
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)

42
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;
}
/// <inheritdoc/>
protected override void OnPointerPressed(PointerPressedEventArgs e)
/// <inheritdoc cref="ItemSelectionEventTriggers.ShouldTriggerSelection(Visual, PointerEventArgs)"/>
/// <seealso cref="UpdateSelectionFromEvent"/>
protected virtual bool ShouldTriggerSelection(Visual selectable, PointerEventArgs eventArgs) => ItemSelectionEventTriggers.ShouldTriggerSelection(selectable, eventArgs);
/// <inheritdoc cref="ItemSelectionEventTriggers.ShouldTriggerSelection(Visual, PointerEventArgs)"/>
protected virtual bool ShouldTriggerSelection(Visual selectable, KeyEventArgs eventArgs) => ItemSelectionEventTriggers.ShouldTriggerSelection(selectable, eventArgs);
/// <inheritdoc cref="SelectingItemsControl.UpdateSelectionFromEvent"/>
/// <seealso cref="SelectingItemsControl.UpdateSelectionFromEvent"/>
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;
}
}

16
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)

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

Loading…
Cancel
Save