Browse Source

Improve pen secondary button handling on list box (#18766)

* Only start ScrollGesture when left click pressed, also `GetCurrentPoint(null)` behaves the same as root visual

* Allow right-click pen to select items on press

* Add context menus to even items on ListBox page for testing

* Avoid global static in UpdateSelectionFromPointerEvent

* Revert "Avoid global static in UpdateSelectionFromPointerEvent"

This reverts commit 2562d73e83.

* Add comment to UpdateSelectionFromPointerEvent

* Use fully mocked rendering for list box test

* Add pen selection tests

* TouchTestHelper should use correct inputs
pull/18892/head
Maxwell Katz 9 months ago
committed by GitHub
parent
commit
338a5ea0a2
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 7
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  2. 12
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  3. 1
      src/Avalonia.Controls/ListBox.cs
  4. 6
      src/Avalonia.Controls/ListBoxItem.cs
  5. 28
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  6. 192
      tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs
  7. 4
      tests/Avalonia.UnitTests/MouseTestHelper.cs
  8. 9
      tests/Avalonia.UnitTests/TouchTestHelper.cs

7
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -5,6 +5,13 @@
x:DataType="viewModels:ListBoxPageViewModel">
<DockPanel>
<DockPanel.Styles>
<Style Selector="ListBox ListBoxItem:nth-child(even)">
<Setter Property="ContextFlyout">
<MenuFlyout>
<MenuItem Header="Hello there" />
</MenuFlyout>
</Setter>
</Style>
<Style Selector="ListBox ListBoxItem:nth-child(5n+3)">
<Setter Property="Foreground" Value="Red" />
<Setter Property="FontWeight" Value="Bold" />

12
src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs

@ -23,7 +23,6 @@ namespace Avalonia.Input.GestureRecognizers
private int _gestureId;
private Point _pointerPressedPoint;
private VelocityTracker? _velocityTracker;
private Visual? _rootTarget;
// Movement per second
private Vector _inertia;
@ -96,13 +95,15 @@ namespace Avalonia.Input.GestureRecognizers
protected override void PointerPressed(PointerPressedEventArgs e)
{
if (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)
var point = e.GetCurrentPoint(null);
if (e.Pointer.Type is PointerType.Touch or PointerType.Pen
&& point.Properties.IsLeftButtonPressed)
{
EndGesture();
_tracking = e.Pointer;
_gestureId = ScrollGestureEventArgs.GetNextFreeId();
_rootTarget = (Visual?)(Target as Visual)?.VisualRoot;
_trackedRootPoint = _pointerPressedPoint = e.GetPosition(_rootTarget);
_trackedRootPoint = _pointerPressedPoint = point.Position;
_velocityTracker = new VelocityTracker();
_velocityTracker?.AddPosition(TimeSpan.FromMilliseconds(e.Timestamp), default);
}
@ -112,7 +113,7 @@ namespace Avalonia.Input.GestureRecognizers
{
if (e.Pointer == _tracking)
{
var rootPoint = e.GetPosition(_rootTarget);
var rootPoint = e.GetPosition(null);
if (!_scrolling)
{
if (CanHorizontallyScroll && Math.Abs(_trackedRootPoint.X - rootPoint.X) > ScrollStartDistance)
@ -159,7 +160,6 @@ namespace Avalonia.Input.GestureRecognizers
Target!.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId));
_gestureId = 0;
_lastMoveTimestamp = null;
_rootTarget = null;
}
}

1
src/Avalonia.Controls/ListBox.cs

@ -166,6 +166,7 @@ namespace Avalonia.Controls
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);

6
src/Avalonia.Controls/ListBoxItem.cs

@ -64,9 +64,11 @@ namespace Avalonia.Controls
if (p.Properties.PointerUpdateKind is PointerUpdateKind.LeftButtonPressed or
PointerUpdateKind.RightButtonPressed)
{
if (p.Pointer.Type == PointerType.Mouse)
if (p.Pointer.Type == PointerType.Mouse
|| (p.Pointer.Type == PointerType.Pen && p.Properties.IsRightButtonPressed))
{
// If the pressed point comes from a mouse, perform the selection immediately.
// 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

28
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -611,17 +611,6 @@ namespace Avalonia.Controls.UnitTests
}.RegisterInNameScope(scope));
}
private static FuncControlTemplate ListBoxItemTemplate()
{
return new FuncControlTemplate<ListBoxItem>((parent, scope) =>
new ContentPresenter
{
Name = "PART_ContentPresenter",
[!ContentPresenter.ContentProperty] = parent[!ListBoxItem.ContentProperty],
[!ContentPresenter.ContentTemplateProperty] = parent[!ListBoxItem.ContentTemplateProperty],
}.RegisterInNameScope(scope));
}
private static FuncControlTemplate ScrollViewerTemplate()
{
return new FuncControlTemplate<ScrollViewer>((parent, scope) =>
@ -645,21 +634,7 @@ namespace Avalonia.Controls.UnitTests
private static void Prepare(ListBox target)
{
target.Width = target.Height = 100;
var root = new TestRoot(target)
{
Resources =
{
{
typeof(ListBoxItem),
new ControlTheme(typeof(ListBoxItem))
{
Setters = { new Setter(ListBoxItem.TemplateProperty, ListBoxItemTemplate()) }
}
}
}
};
var root = new TestRoot(target);
root.LayoutManager.ExecuteInitialLayoutPass();
}
@ -1263,7 +1238,6 @@ namespace Avalonia.Controls.UnitTests
{
new ListBoxItem()
{
Template = ListBoxItemTemplate(),
Content = target,
}
}

192
tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs

@ -6,6 +6,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Input.Platform;
using Avalonia.LogicalTree;
using Avalonia.Styling;
@ -19,30 +20,34 @@ namespace Avalonia.Controls.UnitTests
public class ListBoxTests_Single : ScopedTestBase
{
MouseTestHelper _mouse = new MouseTestHelper();
MouseTestHelper _pen = new MouseTestHelper(PointerType.Pen);
[Fact]
public void Focusing_Item_With_Tab_Should_Not_Select_It()
{
var target = new ListBox
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
ItemsSource = new[] { "Foo", "Bar", "Baz " },
};
var target = new ListBox
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
ItemsSource = new[] { "Foo", "Bar", "Baz " },
};
ApplyTemplate(target);
Prepare(target);
target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs
{
NavigationMethod = NavigationMethod.Tab,
});
target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs
{
NavigationMethod = NavigationMethod.Tab,
});
Assert.Equal(-1, target.SelectedIndex);
Assert.Equal(-1, target.SelectedIndex);
}
}
[Fact]
public void Pressing_Space_On_Focused_Item_With_Ctrl_Pressed_Should_Select_It()
{
using (UnitTestApplication.Start())
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new ListBox
{
@ -50,7 +55,7 @@ namespace Avalonia.Controls.UnitTests
ItemsSource = new[] { "Foo", "Bar", "Baz " },
};
AvaloniaLocator.CurrentMutable.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration());
ApplyTemplate(target);
Prepare(target);
target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs
{
@ -72,7 +77,7 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Clicking_Item_Should_Select_It()
{
using (UnitTestApplication.Start())
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new ListBox
{
@ -80,17 +85,88 @@ namespace Avalonia.Controls.UnitTests
ItemsSource = new[] { "Foo", "Bar", "Baz " },
};
AvaloniaLocator.CurrentMutable.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration());
ApplyTemplate(target);
Prepare(target);
_mouse.Click(target.Presenter.Panel.Children[0]);
Assert.Equal(0, target.SelectedIndex);
}
}
[Fact]
public void Pen_Right_Press_Item_Should_Select_It()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new ListBox
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Width = 20, Height = 10 }),
ItemsSource = new[] { "Foo", "Bar", "Baz " }
};
Prepare(target);
_pen.Down(target.Presenter.Panel.Children[0], MouseButton.Right);
Assert.Equal(0, target.SelectedIndex);
}
}
[Fact]
public void Pen_Left_Press_Item_Should_Not_Select_It()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new ListBox
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Width = 20, Height = 10 }),
ItemsSource = new[] { "Foo", "Bar", "Baz " }
};
Prepare(target);
_pen.Down(target.Presenter.Panel.Children[0]);
Assert.Equal(-1, target.SelectedIndex);
}
}
[Theory]
[InlineData(PointerType.Mouse)]
[InlineData(PointerType.Pen)]
public void Pointer_Right_Click_Should_Select_Item_And_Open_Context(PointerType type)
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var target = new ListBox
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
ItemsSource = new[] { "Foo", "Bar", "Baz " },
ItemTemplate = new FuncDataTemplate<string>((x, _) => new Border { Height = 10 })
};
target.GestureRecognizers.Add(new ScrollGestureRecognizer()
{
CanVerticallyScroll = true, ScrollStartDistance = 50
});
Prepare(target);
var contextRaised = false;
target.AddHandler(Control.ContextRequestedEvent, (sender, args) =>
{
contextRaised = true;
args.Handled = true;
});
var pointer = type == PointerType.Mouse ? _mouse : _pen;
pointer.Click(target.Presenter.Panel.Children[0], MouseButton.Right, position: new Point(5, 5));
Assert.True(contextRaised);
Assert.Equal(0, target.SelectedIndex);
}
}
[Fact]
public void Clicking_Selected_Item_Should_Not_Deselect_It()
{
using (UnitTestApplication.Start())
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new ListBox
{
@ -98,7 +174,7 @@ namespace Avalonia.Controls.UnitTests
ItemsSource = new[] { "Foo", "Bar", "Baz " },
};
AvaloniaLocator.CurrentMutable.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration());
ApplyTemplate(target);
Prepare(target);
target.SelectedIndex = 0;
_mouse.Click(target.Presenter.Panel.Children[0]);
@ -110,7 +186,7 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Clicking_Item_Should_Select_It_When_SelectionMode_Toggle()
{
using (UnitTestApplication.Start())
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new ListBox
{
@ -119,7 +195,7 @@ namespace Avalonia.Controls.UnitTests
SelectionMode = SelectionMode.Single | SelectionMode.Toggle,
};
AvaloniaLocator.CurrentMutable.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration());
ApplyTemplate(target);
Prepare(target);
_mouse.Click(target.Presenter.Panel.Children[0]);
@ -130,7 +206,7 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Clicking_Selected_Item_Should_Deselect_It_When_SelectionMode_Toggle()
{
using (UnitTestApplication.Start())
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new ListBox
{
@ -140,7 +216,7 @@ namespace Avalonia.Controls.UnitTests
};
AvaloniaLocator.CurrentMutable.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration());
ApplyTemplate(target);
Prepare(target);
target.SelectedIndex = 0;
_mouse.Click(target.Presenter.Panel.Children[0]);
@ -152,7 +228,7 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Clicking_Selected_Item_Should_Not_Deselect_It_When_SelectionMode_ToggleAlwaysSelected()
{
using (UnitTestApplication.Start())
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new ListBox
{
@ -161,7 +237,7 @@ namespace Avalonia.Controls.UnitTests
SelectionMode = SelectionMode.Toggle | SelectionMode.AlwaysSelected,
};
AvaloniaLocator.CurrentMutable.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration());
ApplyTemplate(target);
Prepare(target);
target.SelectedIndex = 0;
_mouse.Click(target.Presenter.Panel.Children[0]);
@ -173,7 +249,7 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Clicking_Another_Item_Should_Select_It_When_SelectionMode_Toggle()
{
using (UnitTestApplication.Start())
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new ListBox
{
@ -182,7 +258,7 @@ namespace Avalonia.Controls.UnitTests
SelectionMode = SelectionMode.Single | SelectionMode.Toggle,
};
AvaloniaLocator.CurrentMutable.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration());
ApplyTemplate(target);
Prepare(target);
target.SelectedIndex = 1;
_mouse.Click(target.Presenter.Panel.Children[0]);
@ -194,45 +270,48 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Setting_Item_IsSelected_Sets_ListBox_Selection()
{
var target = new ListBox
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
ItemsSource = new[] { "Foo", "Bar", "Baz " },
};
var target = new ListBox
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
ItemsSource = new[] { "Foo", "Bar", "Baz " },
};
ApplyTemplate(target);
Prepare(target);
((ListBoxItem)target.GetLogicalChildren().ElementAt(1)).IsSelected = true;
((ListBoxItem)target.GetLogicalChildren().ElementAt(1)).IsSelected = true;
Assert.Equal("Bar", target.SelectedItem);
Assert.Equal(1, target.SelectedIndex);
Assert.Equal("Bar", target.SelectedItem);
Assert.Equal(1, target.SelectedIndex);
}
}
[Fact]
public void SelectedItem_Should_Not_Cause_StackOverflow()
{
var viewModel = new TestStackOverflowViewModel()
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Items = new List<string> { "foo", "bar", "baz" }
};
var viewModel = new TestStackOverflowViewModel() { Items = new List<string> { "foo", "bar", "baz" } };
var target = new ListBox
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
DataContext = viewModel,
ItemsSource = viewModel.Items
};
var target = new ListBox
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
DataContext = viewModel,
ItemsSource = viewModel.Items
};
target.Bind(ListBox.SelectedItemProperty,
new Binding("SelectedItem") { Mode = BindingMode.TwoWay });
target.Bind(ListBox.SelectedItemProperty,
new Binding("SelectedItem") { Mode = BindingMode.TwoWay });
Assert.Equal(0, viewModel.SetterInvokedCount);
Assert.Equal(0, viewModel.SetterInvokedCount);
// In Issue #855, a Stackoverflow occurred here.
target.SelectedItem = viewModel.Items[2];
// In Issue #855, a Stackoverflow occurred here.
target.SelectedItem = viewModel.Items[2];
Assert.Equal(viewModel.Items[1], target.SelectedItem);
Assert.Equal(1, viewModel.SetterInvokedCount);
Assert.Equal(viewModel.Items[1], target.SelectedItem);
Assert.Equal(1, viewModel.SetterInvokedCount);
}
}
private class TestStackOverflowViewModel : INotifyPropertyChanged
@ -295,20 +374,11 @@ namespace Avalonia.Controls.UnitTests
}.RegisterInNameScope(scope);
}
private static void ApplyTemplate(ListBox target)
private static void Prepare(ListBox target)
{
// Apply the template to the ListBox itself.
target.ApplyTemplate();
// Then to its inner ScrollViewer.
var scrollViewer = (ScrollViewer)target.GetVisualChildren().Single();
scrollViewer.ApplyTemplate();
// Then make the ScrollViewer create its child.
scrollViewer.Presenter.UpdateChild();
// Now the ItemsPresenter should be registered, so apply its template.
target.Presenter.ApplyTemplate();
target.Width = target.Height = 100;
var root = new TestRoot(target);
root.LayoutManager.ExecuteInitialLayoutPass();
}
}
}

4
tests/Avalonia.UnitTests/MouseTestHelper.cs

@ -4,9 +4,9 @@ using Avalonia.VisualTree;
namespace Avalonia.UnitTests
{
public class MouseTestHelper
public class MouseTestHelper(PointerType pointerType = PointerType.Mouse)
{
private readonly Pointer _pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
private readonly Pointer _pointer = new Pointer(Pointer.GetNextFreeId(), pointerType, true);
private ulong _nextStamp = 1;
private ulong Timestamp() => _nextStamp++;

9
tests/Avalonia.UnitTests/TouchTestHelper.cs

@ -19,7 +19,8 @@ namespace Avalonia.UnitTests
public void Down(Interactive target, Interactive source, Point position = default, KeyModifiers modifiers = default)
{
_pointer.Capture((IInputElement)target);
source.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (Visual)source, position, Timestamp(), PointerPointProperties.None,
source.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (Visual)source, position, Timestamp(),
new(RawInputModifiers.LeftMouseButton, PointerUpdateKind.LeftButtonPressed),
modifiers));
}
@ -28,7 +29,7 @@ namespace Avalonia.UnitTests
public void Move(Interactive target, Interactive source, in Point position, KeyModifiers modifiers = default)
{
var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, (Visual)target, position,
Timestamp(), PointerPointProperties.None, modifiers);
Timestamp(), new(RawInputModifiers.LeftMouseButton, PointerUpdateKind.Other), modifiers);
if (_pointer.CapturedGestureRecognizer != null)
_pointer.CapturedGestureRecognizer.PointerMovedInternal(e);
else
@ -41,8 +42,8 @@ namespace Avalonia.UnitTests
public void Up(Interactive target, Interactive source, Point position = default, KeyModifiers modifiers = default)
{
var e = new PointerReleasedEventArgs(source, _pointer, (Visual)target, position, Timestamp(), PointerPointProperties.None,
modifiers, MouseButton.None);
var e = new PointerReleasedEventArgs(source, _pointer, (Visual)target, position, Timestamp(),
new(RawInputModifiers.None, PointerUpdateKind.LeftButtonReleased), modifiers, MouseButton.Left);
if (_pointer.CapturedGestureRecognizer != null)
_pointer.CapturedGestureRecognizer.PointerReleasedInternal(e);

Loading…
Cancel
Save