From b192368cd550f132c2a00aa66ad7d52d07bc75f4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 3 Jul 2024 09:36:08 +0200 Subject: [PATCH] [macOS][X11] Release mouse capture when dialog shown (#16205) * Added integration test for #14525. * Release mouse capture when dialog shown. Fixes #14525. * Release X11 pointer capture when dialog shown. --- samples/IntegrationTestApp/DelegateCommand.cs | 19 +++++++++++ samples/IntegrationTestApp/MainWindow.axaml | 15 +++++++++ .../IntegrationTestApp/MainWindow.axaml.cs | 33 +++++++++++++++++++ src/Avalonia.Base/Input/MouseDevice.cs | 5 +++ src/Avalonia.Base/Input/TouchDevice.cs | 6 ++++ src/Avalonia.Native/WindowImpl.cs | 7 ++++ src/Avalonia.X11/X11Window.cs | 9 +++++ .../PointerTests.cs | 31 +++++++++++++++++ 8 files changed, 125 insertions(+) create mode 100644 samples/IntegrationTestApp/DelegateCommand.cs create mode 100644 tests/Avalonia.IntegrationTests.Appium/PointerTests.cs diff --git a/samples/IntegrationTestApp/DelegateCommand.cs b/samples/IntegrationTestApp/DelegateCommand.cs new file mode 100644 index 00000000000..5a9c7bc012b --- /dev/null +++ b/samples/IntegrationTestApp/DelegateCommand.cs @@ -0,0 +1,19 @@ +using System; +using System.Windows.Input; + +namespace IntegrationTestApp; + +internal class DelegateCommand : ICommand +{ + private readonly Action _action; + private readonly Func _canExecute; + public DelegateCommand(Action action, Func? canExecute = default) + { + _action = action; + _canExecute = canExecute ?? new(_ => true); + } + + public event EventHandler? CanExecuteChanged { add { } remove { } } + public bool CanExecute(object? parameter) => _canExecute(parameter); + public void Execute(object? parameter) => _action(); +} diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 55c96490d56..87a1ef54e5e 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -137,6 +137,21 @@ + + + + + + Show Dialog + + + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 986eb920a35..73d27cd0180 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Avalonia; @@ -285,5 +286,37 @@ private void OnButtonClick(object? sender, RoutedEventArgs e) if (source?.Name == "RestoreAll") RestoreAll(); } + + private void PointerPageShowDialogPressed(object? sender, PointerPressedEventArgs e) + { + void CaptureLost(object? sender, PointerCaptureLostEventArgs e) + { + PointerCaptureStatus.Text = "None"; + ((Control)sender!).PointerCaptureLost -= CaptureLost; + } + + var captured = e.Pointer.Captured as Control; + + if (captured is not null) + { + captured.PointerCaptureLost += CaptureLost; + } + + PointerCaptureStatus.Text = captured?.ToString() ?? "None"; + + var dialog = new Window + { + Width = 200, + Height = 200, + }; + + dialog.Content = new Button + { + Content = "Close", + Command = new DelegateCommand(() => dialog.Close()), + }; + + dialog.ShowDialog(this); + } } } diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs index 9624bd2ef6a..fc605516061 100644 --- a/src/Avalonia.Base/Input/MouseDevice.cs +++ b/src/Avalonia.Base/Input/MouseDevice.cs @@ -305,5 +305,10 @@ public void Dispose() { return _pointer; } + + internal void PlatformCaptureLost() + { + _pointer.Capture(null); + } } } diff --git a/src/Avalonia.Base/Input/TouchDevice.cs b/src/Avalonia.Base/Input/TouchDevice.cs index cc804572268..8e662ea1b96 100644 --- a/src/Avalonia.Base/Input/TouchDevice.cs +++ b/src/Avalonia.Base/Input/TouchDevice.cs @@ -160,5 +160,11 @@ public void Dispose() ? pointer : null; } + + internal void PlatformCaptureLost() + { + foreach (var pointer in _pointers.Values) + pointer.Capture(null); + } } } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index b18f2e196b6..6fa937c332c 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -225,6 +225,13 @@ public void SetParent(IWindowImpl parent) public void SetEnabled(bool enable) { _native.SetEnabled(enable.AsComBool()); + + // Showing a dialog should result in mouse capture being lost. macOS doesn't have the concept of mouse + // capture, so no we have no OS-level event to hook into. Instead, release the mouse capture when the + // owner window is disabled. This behavior matches win32, which sends a WM_CANCELMODE message when + // EnableWindow(hWnd, false) is called from SetEnabled. + if (!enable && MouseDevice is MouseDevice mouse) + mouse.PlatformCaptureLost(); } public override object TryGetFeature(Type featureType) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 85eda994183..226c600835e 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -1306,6 +1306,15 @@ public void SetEnabled(bool enable) // so setting it again forces the update UpdateMotifHints(); } + else + { + // Showing a dialog should result in pointer capture being lost. We don't currently use XGrabPointer on + // X11 to implement pointer capture, so no we have no OS-level event to hook into. Instead, release the + // pointer capture when the owner window is disabled. This behavior matches win32, which sends a + // WM_CANCELMODE message when EnableWindow(hWnd, false) is called from SetEnabled. + _mouse.PlatformCaptureLost(); + _touch.PlatformCaptureLost(); + } } private void UpdateWMHints() diff --git a/tests/Avalonia.IntegrationTests.Appium/PointerTests.cs b/tests/Avalonia.IntegrationTests.Appium/PointerTests.cs new file mode 100644 index 00000000000..141e9386d4f --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/PointerTests.cs @@ -0,0 +1,31 @@ +using OpenQA.Selenium.Interactions; +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + [Collection("Default")] + public class PointerTests + { + private readonly AppiumDriver _session; + + public PointerTests(DefaultAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("Pointer"); + tab.Click(); + } + + [Fact] + public void Pointer_Capture_Is_Released_When_Showing_Dialog() + { + var button = _session.FindElementByAccessibilityId("PointerPageShowDialog"); + + button.OpenWindowWithClick().Dispose(); + + var status = _session.FindElementByAccessibilityId("PointerCaptureStatus"); + Assert.Equal("None", status.Text); + } + } +}