Browse Source

Fixes/osx thick titlebar pointer events streaming, (tabs interface) #15696 (#19320)

* Added failing test for OSXThickTitleBar drag events outside thick title area

* MacOS. Added event tracking loop for drag events started in thick titlebar (NSToolbar)

* Review fix: forward events to AppKit during tracking loop (#19320)
release/11.3.3
Vladislav Pozdniakov 6 months ago
committed by Julien Lebosquain
parent
commit
92d9737f99
  1. 87
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  2. 1
      samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml
  3. 6
      samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml.cs
  4. 24
      samples/IntegrationTestApp/ShowWindowTest.axaml
  5. 48
      samples/IntegrationTestApp/ShowWindowTest.axaml.cs
  6. 89
      tests/Avalonia.IntegrationTests.Appium/PointerTests_MacOS.cs

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

@ -476,8 +476,95 @@
}
}
- (BOOL)isPointInTitlebar:(NSPoint)windowPoint
{
auto parent = _parent.tryGetWithCast<WindowImpl>();
if (!parent || !_isExtended) {
return NO;
}
AvnView* view = parent->View;
NSPoint viewPoint = [view convertPoint:windowPoint fromView:nil];
double titlebarHeight = [self getExtendedTitleBarHeight];
// Check if click is in titlebar area (top portion of view)
if (viewPoint.y <= titlebarHeight) {
// Verify we're actually in a toolbar-related area
NSView* hitView = [[self findRootView:view] hitTest:windowPoint];
if (hitView) {
NSString* hitViewClass = [hitView className];
if ([hitViewClass containsString:@"Toolbar"] || [hitViewClass containsString:@"Titlebar"]) {
return YES;
}
}
}
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;
}
[super sendEvent:event];
auto parent = _parent.tryGetWithCast<WindowImpl>();

1
samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml

@ -9,6 +9,7 @@
<CheckBox Name="WindowForceSystemChrome" Content="Force SystemChrome" />
<CheckBox Name="WindowPreferSystemChrome" Content="Prefer SystemChrome" />
<CheckBox Name="WindowMacThickSystemChrome" Content="Mac Thick SystemChrome" />
<CheckBox Name="WindowShowTitleAreaControl" Content="Show Title Area Control" />
<TextBox Name="WindowTitleBarHeightHint" Text="-1" Watermark="In dips" />
<Button Name="ApplyWindowDecorations"
Content="Apply decorations on this Window"

6
samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml.cs

@ -21,6 +21,12 @@ public partial class WindowDecorationsPage : UserControl
| (WindowForceSystemChrome.IsChecked == true ? ExtendClientAreaChromeHints.SystemChrome : 0)
| (WindowPreferSystemChrome.IsChecked == true ? ExtendClientAreaChromeHints.PreferSystemChrome : 0)
| (WindowMacThickSystemChrome.IsChecked == true ? ExtendClientAreaChromeHints.OSXThickTitleBar : 0);
if (window is ShowWindowTest showWindowTest && WindowShowTitleAreaControl.IsChecked == true)
{
showWindowTest.ShowTitleAreaControl();
}
AdjustOffsets(window);
window.Background = Brushes.Transparent;

24
samples/IntegrationTestApp/ShowWindowTest.axaml

@ -5,8 +5,17 @@
Name="SecondaryWindow"
x:DataType="Window"
Title="Show Window Test">
<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">
<Grid>
<Grid Name="TitleAreaControl" IsVisible="False"
Background="LightBlue" VerticalAlignment="Top" ZIndex="100"
Margin="0,0,0,0">
<TextBlock Text="Title Area Control (Tabs)"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="DarkBlue" FontWeight="Bold" />
</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">
<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}" />
@ -53,12 +62,19 @@
<Label Grid.Row="11" Content="MeasuredWith:" />
<TextBlock Grid.Column="1" Grid.Row="11" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" />
<StackPanel Orientation="Horizontal" Grid.Row="12">
<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="0" Grid.Row="13">Mouse Release Event Count</Label>
<TextBox Name="MouseReleaseCount" Grid.Column="1" Grid.Row="13" IsReadOnly="True" Text="0" />
<StackPanel Orientation="Horizontal" Grid.Row="14" Grid.ColumnSpan="2">
<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>
</StackPanel>
</Grid>
</integrationTestApp:MeasureBorder>
</integrationTestApp:MeasureBorder>
</Grid>
</Window>

48
samples/IntegrationTestApp/ShowWindowTest.axaml.cs

@ -30,6 +30,8 @@ namespace IntegrationTestApp
{
private readonly DispatcherTimer? _timer;
private readonly TextBox? _orderTextBox;
private int _mouseMoveCount;
private int _mouseReleaseCount;
public ShowWindowTest()
{
@ -37,6 +39,10 @@ namespace IntegrationTestApp
DataContext = this;
PositionChanged += (s, e) => CurrentPosition.Text = $"{Position}";
PointerMoved += OnPointerMoved;
PointerReleased += OnPointerReleased;
PointerExited += (_, e) => ResetCounters();
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
_orderTextBox = CurrentOrder;
@ -74,5 +80,47 @@ namespace IntegrationTestApp
private void AddToWidth_Click(object? sender, RoutedEventArgs e) => Width = Bounds.Width + 10;
private void AddToHeight_Click(object? sender, RoutedEventArgs e) => Height = Bounds.Height + 10;
private void OnPointerMoved(object? sender, Avalonia.Input.PointerEventArgs e)
{
_mouseMoveCount++;
UpdateCounterDisplays();
}
private void OnPointerReleased(object? sender, Avalonia.Input.PointerReleasedEventArgs e)
{
_mouseReleaseCount++;
UpdateCounterDisplays();
}
public void ResetCounters()
{
_mouseMoveCount = 0;
_mouseReleaseCount = 0;
UpdateCounterDisplays();
}
private void UpdateCounterDisplays()
{
var mouseMoveCountTextBox = this.FindControl<TextBox>("MouseMoveCount");
var mouseReleaseCountTextBox = this.FindControl<TextBox>("MouseReleaseCount");
if (mouseMoveCountTextBox != null)
mouseMoveCountTextBox.Text = _mouseMoveCount.ToString();
if (mouseReleaseCountTextBox != null)
mouseReleaseCountTextBox.Text = _mouseReleaseCount.ToString();
}
public void ShowTitleAreaControl()
{
var titleAreaControl = this.FindControl<Grid>("TitleAreaControl");
if (titleAreaControl == null) return;
titleAreaControl.IsVisible = true;
var titleBarHeight = ExtendClientAreaTitleBarHeightHint > 0 ? ExtendClientAreaTitleBarHeightHint : 30;
titleAreaControl.Margin = new Thickness(110, -titleBarHeight, 8, 0);
titleAreaControl.Height = titleBarHeight;
}
}
}

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

@ -0,0 +1,89 @@
using System;
using System.Threading;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Interactions;
using Xunit;
namespace Avalonia.IntegrationTests.Appium;
[Collection("WindowDecorations")]
public class PointerTests_MacOS : TestBase, IDisposable
{
public PointerTests_MacOS(DefaultAppFixture fixture)
: base(fixture, "Window Decorations")
{
}
[PlatformFact(TestPlatforms.MacOS)]
public void OSXThickTitleBar_Pointer_Events_Continue_Outside_Window_During_Drag()
{
// issue #15696
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);
new Actions(Session).MoveToElement(secondaryWindow).Perform();
new Actions(Session).MoveToElement(titleAreaControl).Perform();
new Actions(Session).DragAndDropToOffset(titleAreaControl, 50, -100).Perform();
var finalMoveCount = GetMoveCount(secondaryWindow);
var finalReleaseCount = GetReleaseCount(secondaryWindow);
Assert.True(finalMoveCount >= 10, $"Expected at least 10 new mouse move events outside window, got {finalMoveCount})");
Assert.Equal(1, finalReleaseCount);
secondaryWindow.FindElementByAccessibilityId("_XCUI:CloseWindow").Click();
}
private void SetParameters(
bool extendClientArea,
bool forceSystemChrome,
bool preferSystemChrome,
bool macOsThickSystemChrome,
bool showTitleAreaControl)
{
var extendClientAreaCheckBox = Session.FindElementByAccessibilityId("WindowExtendClientAreaToDecorationsHint");
var forceSystemChromeCheckBox = Session.FindElementByAccessibilityId("WindowForceSystemChrome");
var preferSystemChromeCheckBox = Session.FindElementByAccessibilityId("WindowPreferSystemChrome");
var macOsThickSystemChromeCheckBox = Session.FindElementByAccessibilityId("WindowMacThickSystemChrome");
var showTitleAreaControlCheckBox = Session.FindElementByAccessibilityId("WindowShowTitleAreaControl");
if (extendClientAreaCheckBox.GetIsChecked() != extendClientArea)
extendClientAreaCheckBox.Click();
if (forceSystemChromeCheckBox.GetIsChecked() != forceSystemChrome)
forceSystemChromeCheckBox.Click();
if (preferSystemChromeCheckBox.GetIsChecked() != preferSystemChrome)
preferSystemChromeCheckBox.Click();
if (macOsThickSystemChromeCheckBox.GetIsChecked() != macOsThickSystemChrome)
macOsThickSystemChromeCheckBox.Click();
if (showTitleAreaControlCheckBox.GetIsChecked() != showTitleAreaControl)
showTitleAreaControlCheckBox.Click();
}
private int GetMoveCount(AppiumWebElement window)
{
var mouseMoveCountTextBox = window.FindElementByAccessibilityId("MouseMoveCount");
return int.Parse(mouseMoveCountTextBox.Text ?? "0");
}
private int GetReleaseCount(AppiumWebElement window)
{
var mouseReleaseCountTextBox = window.FindElementByAccessibilityId("MouseReleaseCount");
return int.Parse(mouseReleaseCountTextBox.Text ?? "0");
}
public void Dispose()
{
SetParameters(false, false, false, false, false);
var applyButton = Session.FindElementByAccessibilityId("ApplyWindowDecorations");
applyButton.Click();
}
}
Loading…
Cancel
Save