Browse Source

Fix macOS thick titlebar mouse event duplication (#19447)

* Added failing test for OSXThickTitleBar single title area click produce double click #19320

* Fixed OSXThickTitleBar title area click duplication and event delays until the event-tracking-loop is completed

* IntegrationTestApp. Move event counter controls to a separate column. Fixes Changing_Size_Should_Not_Change_Position test

* Move pointer tests to Default group to avoid interference

* Try to fix CI crash

* Try disabling test

* Fix CI test. Collection back to Default

* CI fix. Return back empty test.

* CI fix. Minimal bug test

* CI test. Add double click test

* CI fix. Remove double click test.

---------

Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
pull/19477/head
Vladislav Pozdniakov 6 months ago
committed by GitHub
parent
commit
4f666f0ba1
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 98
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  2. 18
      samples/IntegrationTestApp/ShowWindowTest.axaml
  3. 27
      samples/IntegrationTestApp/ShowWindowTest.axaml.cs
  4. 5
      tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs
  5. 53
      tests/Avalonia.IntegrationTests.Appium/PointerTests_MacOS.cs

98
native/Avalonia.Native/src/OSX/AvnWindow.mm

@ -35,6 +35,7 @@
bool _canBecomeKeyWindow;
bool _isExtended;
bool _isTransitioningToFullScreen;
bool _isTitlebarSession;
AvnMenu* _menu;
IAvnAutomationPeer* _automationPeer;
AvnAutomationNode* _automationNode;
@ -501,68 +502,10 @@
return NO;
}
- (void)forwardToAvnView:(NSEvent *)event
{
auto parent = _parent.tryGetWithCast<WindowImpl>();
if (!parent) {
return;
}
switch(event.type) {
case NSEventTypeLeftMouseDown:
[parent->View mouseDown:event];
break;
case NSEventTypeLeftMouseUp:
[parent->View mouseUp:event];
break;
case NSEventTypeLeftMouseDragged:
[parent->View mouseDragged:event];
break;
case NSEventTypeRightMouseDown:
[parent->View rightMouseDown:event];
break;
case NSEventTypeRightMouseUp:
[parent->View rightMouseUp:event];
break;
case NSEventTypeRightMouseDragged:
[parent->View rightMouseDragged:event];
break;
case NSEventTypeOtherMouseDown:
[parent->View otherMouseDown:event];
break;
case NSEventTypeOtherMouseUp:
[parent->View otherMouseUp:event];
break;
case NSEventTypeOtherMouseDragged:
[parent->View otherMouseDragged:event];
break;
case NSEventTypeMouseMoved:
[parent->View mouseMoved:event];
break;
default:
break;
}
}
- (void)sendEvent:(NSEvent *_Nonnull)event
{
// Event-tracking loop for thick titlebar mouse events
if (event.type == NSEventTypeLeftMouseDown && [self isPointInTitlebar:event.locationInWindow])
{
NSEventMask mask = NSEventMaskLeftMouseDragged | NSEventMaskLeftMouseUp;
NSEvent *ev = event;
while (ev.type != NSEventTypeLeftMouseUp)
{
[self forwardToAvnView:ev];
[super sendEvent:ev];
ev = [NSApp nextEventMatchingMask:mask
untilDate:[NSDate distantFuture]
inMode:NSEventTrackingRunLoopMode
dequeue:YES];
}
[self forwardToAvnView:ev];
[super sendEvent:ev];
return;
if (event.type == NSEventTypeLeftMouseDown) {
_isTitlebarSession = [self isPointInTitlebar:event.locationInWindow];
}
[super sendEvent:event];
@ -603,6 +546,37 @@
}
break;
case NSEventTypeLeftMouseDragged:
case NSEventTypeMouseMoved:
case NSEventTypeLeftMouseUp:
{
// Usually NSToolbar events are passed natively to AvnView when the mouse is inside the control.
// When a drag operation started in NSToolbar leaves the control region, the view does not get any
// events. We will detect this scenario and pass events ourselves.
if(!_isTitlebarSession || [self isPointInTitlebar:event.locationInWindow])
break;
AvnView* view = parent->View;
if(!view)
break;
if(event.type == NSEventTypeLeftMouseDragged)
{
[view mouseDragged:event];
}
else if(event.type == NSEventTypeMouseMoved)
{
[view mouseMoved:event];
}
else if(event.type == NSEventTypeLeftMouseUp)
{
[view mouseUp:event];
}
}
break;
case NSEventTypeMouseEntered:
{
parent->UpdateCursor();
@ -618,6 +592,10 @@
default:
break;
}
if(event.type == NSEventTypeLeftMouseUp) {
_isTitlebarSession = NO;
}
}
}

18
samples/IntegrationTestApp/ShowWindowTest.axaml

@ -15,7 +15,7 @@
</Grid>
<integrationTestApp:MeasureBorder Name="MyBorder" Background="{DynamicResource SystemRegionBrush}">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid ColumnDefinitions="Auto,Auto,50,Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Label Grid.Column="0" Grid.Row="1">Client Size</Label>
<TextBox Name="CurrentClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
Text="{Binding ClientSize, Mode=OneWay}" />
@ -62,13 +62,19 @@
<Label Grid.Row="11" Content="MeasuredWith:" />
<TextBlock Grid.Column="1" Grid.Row="11" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" />
<Label Grid.Column="0" Grid.Row="12">Mouse Move Event Count</Label>
<TextBox Name="MouseMoveCount" Grid.Column="1" Grid.Row="12" IsReadOnly="True" Text="0" />
<Label Grid.Column="3" Grid.Row="1">Mouse Move Event Count</Label>
<TextBox Name="MouseMoveCount" Grid.Column="4" Grid.Row="1" IsReadOnly="True" Text="0" />
<Label Grid.Column="0" Grid.Row="13">Mouse Release Event Count</Label>
<TextBox Name="MouseReleaseCount" Grid.Column="1" Grid.Row="13" IsReadOnly="True" Text="0" />
<Label Grid.Column="3" Grid.Row="2">Mouse Down Event Count</Label>
<TextBox Name="MouseDownCount" Grid.Column="4" Grid.Row="2" IsReadOnly="True" Text="0" />
<StackPanel Orientation="Horizontal" Grid.Row="14" Grid.ColumnSpan="2">
<Label Grid.Column="3" Grid.Row="3">Mouse Release Event Count</Label>
<TextBox Name="MouseReleaseCount" Grid.Column="4" Grid.Row="3" IsReadOnly="True" Text="0" />
<Label Grid.Column="3" Grid.Row="4">Double-Click Event Count</Label>
<TextBox Name="DoubleClickCount" Grid.Column="4" Grid.Row="4" IsReadOnly="True" Text="0" />
<StackPanel Orientation="Horizontal" Grid.Row="12" Grid.ColumnSpan="5">
<Button Name="HideButton" Command="{Binding $parent[Window].Hide}">Hide</Button>
<Button Name="AddToWidth" Click="AddToWidth_Click">Add to Width</Button>
<Button Name="AddToHeight" Click="AddToHeight_Click">Add to Height</Button>

27
samples/IntegrationTestApp/ShowWindowTest.axaml.cs

@ -2,6 +2,7 @@ using System;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Threading;
@ -32,6 +33,8 @@ namespace IntegrationTestApp
private readonly TextBox? _orderTextBox;
private int _mouseMoveCount;
private int _mouseReleaseCount;
private int _doubleClickCount;
private int _mouseDownCount;
public ShowWindowTest()
{
@ -40,8 +43,10 @@ namespace IntegrationTestApp
PositionChanged += (s, e) => CurrentPosition.Text = $"{Position}";
PointerMoved += OnPointerMoved;
PointerPressed += OnPointerPressed;
PointerReleased += OnPointerReleased;
PointerExited += (_, e) => ResetCounters();
DoubleTapped += OnDoubleTapped;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
@ -87,6 +92,12 @@ namespace IntegrationTestApp
UpdateCounterDisplays();
}
private void OnPointerPressed(object? sender, Avalonia.Input.PointerPressedEventArgs e)
{
_mouseDownCount++;
UpdateCounterDisplays();
}
private void OnPointerReleased(object? sender, Avalonia.Input.PointerReleasedEventArgs e)
{
_mouseReleaseCount++;
@ -97,19 +108,35 @@ namespace IntegrationTestApp
{
_mouseMoveCount = 0;
_mouseReleaseCount = 0;
_doubleClickCount = 0;
_mouseDownCount = 0;
UpdateCounterDisplays();
}
private void OnDoubleTapped(object? sender, Avalonia.Input.TappedEventArgs e)
{
_doubleClickCount++;
UpdateCounterDisplays();
}
private void UpdateCounterDisplays()
{
var mouseMoveCountTextBox = this.FindControl<TextBox>("MouseMoveCount");
var mouseDownCountTextBox = this.FindControl<TextBox>("MouseDownCount");
var mouseReleaseCountTextBox = this.FindControl<TextBox>("MouseReleaseCount");
var doubleClickCountTextBox = this.FindControl<TextBox>("DoubleClickCount");
if (mouseMoveCountTextBox != null)
mouseMoveCountTextBox.Text = _mouseMoveCount.ToString();
if (mouseDownCountTextBox != null)
mouseDownCountTextBox.Text = _mouseDownCount.ToString();
if (mouseReleaseCountTextBox != null)
mouseReleaseCountTextBox.Text = _mouseReleaseCount.ToString();
if (doubleClickCountTextBox != null)
doubleClickCountTextBox.Text = _doubleClickCount.ToString();
}
public void ShowTitleAreaControl()

5
tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs

@ -239,6 +239,11 @@ namespace Avalonia.IntegrationTests.Appium
// does. On Windows, Click() seems to fail with the WindowState checkbox for some reason.
new Actions(element.WrappedDriver).MoveToElement(element).Click().Perform();
}
public static void SendDoubleClick(this AppiumWebElement element)
{
new Actions(element.WrappedDriver).MoveToElement(element).DoubleClick().Perform();
}
public static void MovePointerOver(this AppiumWebElement element)
{

53
tests/Avalonia.IntegrationTests.Appium/PointerTests_MacOS.cs

@ -6,7 +6,7 @@ using Xunit;
namespace Avalonia.IntegrationTests.Appium;
[Collection("WindowDecorations")]
[Collection("Default")]
public class PointerTests_MacOS : TestBase, IDisposable
{
public PointerTests_MacOS(DefaultAppFixture fixture)
@ -42,6 +42,45 @@ public class PointerTests_MacOS : TestBase, IDisposable
secondaryWindow.FindElementByAccessibilityId("_XCUI:CloseWindow").Click();
}
[PlatformFact(TestPlatforms.MacOS)]
public void OSXThickTitleBar_Single_Click_Does_Not_Generate_DoubleTapped_Event()
{
SetParameters(true, false, true, true, true);
var showNewWindowDecorations = Session.FindElementByAccessibilityId("ShowNewWindowDecorations");
showNewWindowDecorations.Click();
Thread.Sleep(1000);
var secondaryWindow = Session.GetWindowById("SecondaryWindow");
var titleAreaControl = secondaryWindow.FindElementByAccessibilityId("TitleAreaControl");
Assert.NotNull(titleAreaControl);
// Verify initial state - counters should be 0
var initialDoubleClickCount = GetDoubleClickCount(secondaryWindow);
var initialReleaseCount = GetReleaseCount(secondaryWindow);
var initialMouseDownCount = GetMouseDownCount(secondaryWindow);
Assert.Equal(0, initialDoubleClickCount);
Assert.Equal(0, initialReleaseCount);
Assert.Equal(0, initialMouseDownCount);
// Perform a single click in titlebar area
secondaryWindow.MovePointerOver();
titleAreaControl.MovePointerOver();
titleAreaControl.SendClick();
Thread.Sleep(800);
// After first single click - mouse down = 1, release = 1, double-click = 0
var afterFirstClickMouseDownCount = GetMouseDownCount(secondaryWindow);
var afterFirstClickReleaseCount = GetReleaseCount(secondaryWindow);
var afterFirstClickDoubleClickCount = GetDoubleClickCount(secondaryWindow);
Assert.Equal(1, afterFirstClickMouseDownCount);
Assert.Equal(1, afterFirstClickReleaseCount);
Assert.Equal(0, afterFirstClickDoubleClickCount);
secondaryWindow.FindElementByAccessibilityId("_XCUI:CloseWindow").Click();
}
private void SetParameters(
bool extendClientArea,
@ -79,6 +118,18 @@ public class PointerTests_MacOS : TestBase, IDisposable
var mouseReleaseCountTextBox = window.FindElementByAccessibilityId("MouseReleaseCount");
return int.Parse(mouseReleaseCountTextBox.Text ?? "0");
}
private int GetMouseDownCount(AppiumWebElement window)
{
var mouseDownCountTextBox = window.FindElementByAccessibilityId("MouseDownCount");
return int.Parse(mouseDownCountTextBox.Text ?? "0");
}
private int GetDoubleClickCount(AppiumWebElement window)
{
var doubleClickCountTextBox = window.FindElementByAccessibilityId("DoubleClickCount");
return int.Parse(doubleClickCountTextBox.Text ?? "0");
}
public void Dispose()
{

Loading…
Cancel
Save