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
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,7 +4,7 @@ using Avalonia.VisualTree;
|
||||
|
||||
namespace Avalonia.Input
|
||||
{
|
||||
public class TappedEventArgs : RoutedEventArgs
|
||||
public class TappedEventArgs : RoutedEventArgs, IKeyModifiersEventArgs
|
||||
{
|
||||
private readonly PointerEventArgs lastPointerEventArgs;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
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;
|
||||
UpdateSelectionFromEvent(e);
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
base.OnKeyDown(e);
|
||||
UpdateSelectionFromEvent(e);
|
||||
}
|
||||
|
||||
protected bool UpdateSelectionFromEvent(RoutedEventArgs e) => SelectingItemsControl.ItemsControlFromItemContainer(this)?.UpdateSelectionFromEvent(this, e) ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
protected override bool ShouldTriggerSelection(Visual selectable, PointerEventArgs e) =>
|
||||
e.Properties.PointerUpdateKind is PointerUpdateKind.LeftButtonPressed or PointerUpdateKind.LeftButtonReleased && base.ShouldTriggerSelection(selectable, e);
|
||||
|
||||
public override bool UpdateSelectionFromEvent(Control container, RoutedEventArgs eventArgs)
|
||||
{
|
||||
base.OnGotFocus(e);
|
||||
|
||||
if (e.NavigationMethod == NavigationMethod.Directional)
|
||||
if (eventArgs is GotFocusEventArgs { NavigationMethod: not NavigationMethod.Directional })
|
||||
{
|
||||
e.Handled = UpdateSelectionFromEventSource(e.Source);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
|
||||
if (e.Source is Visual source)
|
||||
{
|
||||
var point = e.GetCurrentPoint(source);
|
||||
|
||||
if (point.Properties.IsLeftButtonPressed)
|
||||
{
|
||||
e.Handled = UpdateSelectionFromEventSource(e.Source);
|
||||
}
|
||||
}
|
||||
return base.UpdateSelectionFromEvent(container, eventArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 (e.Source is Visual source)
|
||||
if (eventArgs.Handled)
|
||||
{
|
||||
var point = e.GetCurrentPoint(source);
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
switch (eventArgs)
|
||||
{
|
||||
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 });
|
||||
|
||||
eventArgs.Handled = true;
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user