diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index f9306abc56..1edc3b5ec2 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -73,6 +73,12 @@ baseline/netstandard2.0/Avalonia.Controls.dll target/netstandard2.0/Avalonia.Controls.dll + + CP0006 + M:Avalonia.Controls.Primitives.IPopupHost.TakeFocus + baseline/netstandard2.0/Avalonia.Controls.dll + target/netstandard2.0/Avalonia.Controls.dll + CP0009 T:Avalonia.Diagnostics.StyleDiagnostics diff --git a/samples/IntegrationTestApp/Embedding/INativeControlFactory.cs b/samples/IntegrationTestApp/Embedding/INativeControlFactory.cs deleted file mode 100644 index 4e6b290f48..0000000000 --- a/samples/IntegrationTestApp/Embedding/INativeControlFactory.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; -using Avalonia.Platform; - -namespace IntegrationTestApp.Embedding; - -internal interface INativeControlFactory -{ - IPlatformHandle CreateControl(IPlatformHandle parent, Func createDefault); -} diff --git a/samples/IntegrationTestApp/Embedding/INativeTextBoxFactory.cs b/samples/IntegrationTestApp/Embedding/INativeTextBoxFactory.cs new file mode 100644 index 0000000000..7a50ffc2e2 --- /dev/null +++ b/samples/IntegrationTestApp/Embedding/INativeTextBoxFactory.cs @@ -0,0 +1,18 @@ +using System; +using Avalonia.Platform; + +namespace IntegrationTestApp.Embedding; + +internal interface INativeTextBoxImpl +{ + IPlatformHandle Handle { get; } + string Text { get; set; } + event EventHandler? ContextMenuRequested; + event EventHandler? Hovered; + event EventHandler? PointerExited; +} + +internal interface INativeTextBoxFactory +{ + INativeTextBoxImpl CreateControl(IPlatformHandle parent); +} diff --git a/samples/IntegrationTestApp/Embedding/MacOSTextBoxFactory.cs b/samples/IntegrationTestApp/Embedding/MacOSTextBoxFactory.cs index c236055ce9..df43c7b794 100644 --- a/samples/IntegrationTestApp/Embedding/MacOSTextBoxFactory.cs +++ b/samples/IntegrationTestApp/Embedding/MacOSTextBoxFactory.cs @@ -1,20 +1,77 @@ using System; -using System.Text; using Avalonia.Platform; +using Avalonia.Threading; using MonoMac.AppKit; -using MonoMac.WebKit; +using MonoMac.Foundation; namespace IntegrationTestApp.Embedding; -internal class MacOSTextBoxFactory : INativeControlFactory +internal class MacOSTextBoxFactory : INativeTextBoxFactory { - public IPlatformHandle CreateControl(IPlatformHandle parent, Func createDefault) + public INativeTextBoxImpl CreateControl(IPlatformHandle parent) { MacHelper.EnsureInitialized(); + return new MacOSTextBox(); + } + + private class MacOSTextBox : NSTextView, INativeTextBoxImpl + { + private DispatcherTimer _timer; + + public MacOSTextBox() + { + TextStorage.Append(new("Native text box")); + Handle = new MacOSViewHandle(this); + _timer = new DispatcherTimer(); + _timer.Interval = TimeSpan.FromMilliseconds(400); + _timer.Tick += (_, _) => + { + Hovered?.Invoke(this, EventArgs.Empty); + _timer.Stop(); + }; + } + + public new IPlatformHandle Handle { get; } + + public string Text + { + get => TextStorage.Value; + set => TextStorage.Replace(new NSRange(0, TextStorage.Length), value); + } + + public event EventHandler? ContextMenuRequested; + public event EventHandler? Hovered; + public event EventHandler? PointerExited; + + public override void MouseEntered(NSEvent theEvent) + { + _timer.Stop(); + _timer.Start(); + base.MouseEntered(theEvent); + } + + public override void MouseExited(NSEvent theEvent) + { + _timer.Stop(); + PointerExited?.Invoke(this, EventArgs.Empty); + base.MouseExited(theEvent); + } + + public override void MouseMoved(NSEvent theEvent) + { + _timer.Stop(); + _timer.Start(); + base.MouseMoved(theEvent); + } - var textView = new NSTextView(); - textView.TextStorage.Append(new("Native text box")); + public override void RightMouseDown(NSEvent theEvent) + { + ContextMenuRequested?.Invoke(this, EventArgs.Empty); + } - return new MacOSViewHandle(textView); + public override void RightMouseUp(NSEvent theEvent) + { + // Don't call base to prevent default action. + } } } diff --git a/samples/IntegrationTestApp/Embedding/NativeTextBox.cs b/samples/IntegrationTestApp/Embedding/NativeTextBox.cs index 8bbc6dc560..f995d53c2b 100644 --- a/samples/IntegrationTestApp/Embedding/NativeTextBox.cs +++ b/samples/IntegrationTestApp/Embedding/NativeTextBox.cs @@ -1,20 +1,86 @@ -using Avalonia.Controls; +using System; +using Avalonia.Controls; using Avalonia.Platform; namespace IntegrationTestApp.Embedding; internal class NativeTextBox : NativeControlHost { - public static INativeControlFactory? Factory { get; set; } + private ContextMenu? _contextMenu; + private INativeTextBoxImpl? _impl; + private TextBlock _tipTextBlock; + private string _initialText = string.Empty; + + public NativeTextBox() + { + _tipTextBlock = new TextBlock + { + Text = "Avalonia ToolTip", + Name = "NativeTextBoxToolTip", + }; + + ToolTip.SetTip(this, _tipTextBlock); + ToolTip.SetShowDelay(this, 1000); + ToolTip.SetServiceEnabled(this, false); + } + + public string Text + { + get => _impl?.Text ?? _initialText; + set + { + if (_impl is not null) + _impl.Text = value; + else + _initialText = value; + } + } + + public static INativeTextBoxFactory? Factory { get; set; } protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent) { - return Factory?.CreateControl(parent, () => base.CreateNativeControlCore(parent)) - ?? base.CreateNativeControlCore(parent); + if (Factory is null) + return base.CreateNativeControlCore(parent); + + _impl = Factory.CreateControl(parent); + _impl.Text = _initialText; + _impl.ContextMenuRequested += OnContextMenuRequested; + _impl.Hovered += OnHovered; + _impl.PointerExited += OnPointerExited; + return _impl.Handle; } protected override void DestroyNativeControlCore(IPlatformHandle control) { base.DestroyNativeControlCore(control); } + + private void OnContextMenuRequested(object? sender, EventArgs e) + { + if (_contextMenu is null) + { + var menuItem = new MenuItem { Header = "Custom Menu Item" }; + menuItem.Click += (s, e) => _impl!.Text = "Context menu item clicked"; + + _contextMenu = new ContextMenu + { + Name = "NativeTextBoxContextMenu", + Items = { menuItem } + }; + } + + ToolTip.SetIsOpen(this, false); + _contextMenu.Open(this); + } + + private void OnHovered(object? sender, EventArgs e) + { + ToolTip.SetIsOpen(this, true); + } + + private void OnPointerExited(object? sender, EventArgs e) + { + ToolTip.SetIsOpen(this, false); + } } diff --git a/samples/IntegrationTestApp/Embedding/Win32TextBoxFactory.cs b/samples/IntegrationTestApp/Embedding/Win32TextBoxFactory.cs index e29699de3f..4818115922 100644 --- a/samples/IntegrationTestApp/Embedding/Win32TextBoxFactory.cs +++ b/samples/IntegrationTestApp/Embedding/Win32TextBoxFactory.cs @@ -1,21 +1,89 @@ using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; using System.Text; +using Avalonia.Controls; using Avalonia.Platform; +using static IntegrationTestApp.Embedding.WinApi; namespace IntegrationTestApp.Embedding; -internal class Win32TextBoxFactory : INativeControlFactory +internal class Win32TextBoxFactory : INativeTextBoxFactory { - public IPlatformHandle CreateControl(IPlatformHandle parent, Func createDefault) + public INativeTextBoxImpl CreateControl(IPlatformHandle parent) { - var handle = WinApi.CreateWindowEx(0, "EDIT", - @"Native text box", - (uint)(WinApi.WindowStyles.WS_CHILD | WinApi.WindowStyles.WS_VISIBLE | WinApi.WindowStyles.WS_BORDER), - 0, 0, 1, 1, - parent.Handle, - IntPtr.Zero, - WinApi.GetModuleHandle(null), - IntPtr.Zero); - return new Win32WindowControlHandle(handle, "HWND"); + return new Win32TextBox(parent); + } + + private class Win32TextBox : INativeTextBoxImpl + { + private readonly IntPtr _oldWndProc; + private readonly WndProcDelegate _wndProc; + private TRACKMOUSEEVENT _trackMouseEvent; + + public Win32TextBox(IPlatformHandle parent) + { + var handle = CreateWindowEx(0, "EDIT", + string.Empty, + (uint)(WinApi.WindowStyles.WS_CHILD | WinApi.WindowStyles.WS_VISIBLE | WinApi.WindowStyles.WS_BORDER), + 0, 0, 1, 1, + parent.Handle, + IntPtr.Zero, + GetModuleHandle(null), + IntPtr.Zero); + + _wndProc = new(WndProc); + _oldWndProc = SetWindowLongPtr(handle, WinApi.GWL_WNDPROC, Marshal.GetFunctionPointerForDelegate(_wndProc)); + + _trackMouseEvent.cbSize = Marshal.SizeOf(); + _trackMouseEvent.dwFlags = TME_HOVER | TME_LEAVE; + _trackMouseEvent.hwndTrack = handle; + _trackMouseEvent.dwHoverTime = 400; + + Handle = new Win32WindowControlHandle(handle, "HWND"); + } + + public IPlatformHandle Handle { get; } + + public string Text + { + get + { + var sb = new StringBuilder(256); + GetWindowText(Handle.Handle, sb, sb.Capacity); + return sb.ToString(); + } + set => SetWindowText(Handle.Handle, value); + } + + public event EventHandler? ContextMenuRequested; + public event EventHandler? Hovered; + public event EventHandler? PointerExited; + + private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + switch (msg) + { + case WM_CONTEXTMENU: + if (ContextMenuRequested is not null) + { + ContextMenuRequested?.Invoke(this, EventArgs.Empty); + return IntPtr.Zero; + } + break; + case WM_MOUSELEAVE: + PointerExited?.Invoke(this, EventArgs.Empty); + break; + case WM_MOUSEHOVER: + Hovered?.Invoke(this, EventArgs.Empty); + break; + case WM_MOUSEMOVE: + TrackMouseEvent(ref _trackMouseEvent); + break; + + } + + return CallWindowProc(_oldWndProc, hWnd, msg, wParam, lParam); + } } } diff --git a/samples/IntegrationTestApp/Embedding/WinApi.cs b/samples/IntegrationTestApp/Embedding/WinApi.cs index ab51988df9..529b905096 100644 --- a/samples/IntegrationTestApp/Embedding/WinApi.cs +++ b/samples/IntegrationTestApp/Embedding/WinApi.cs @@ -1,10 +1,21 @@ using System; using System.Runtime.InteropServices; +using System.Text; namespace IntegrationTestApp.Embedding; internal class WinApi { + public const int GWL_WNDPROC = -4; + public const uint TME_HOVER = 1; + public const uint TME_LEAVE = 2; + public const uint WM_CONTEXTMENU = 0x007B; + public const uint WM_MOUSELEAVE = 0x02A3; + public const uint WM_MOUSEHOVER = 0x02A1; + public const uint WM_MOUSEMOVE = 0x0200; + + public delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + [Flags] public enum WindowStyles : uint { @@ -59,12 +70,19 @@ internal class WinApi WS_EX_NOACTIVATE = 0x08000000 } + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + [DllImport("user32.dll", SetLastError = true)] public static extern bool DestroyWindow(IntPtr hwnd); [DllImport("kernel32.dll")] public static extern IntPtr GetModuleHandle(string? lpModuleName); + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr CreateWindowEx( int dwExStyle, @@ -79,4 +97,22 @@ internal class WinApi IntPtr hMenu, IntPtr hInstance, IntPtr lpParam); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern bool SetWindowText(IntPtr hwnd, String lpString); + + [DllImport("user32.dll")] + public static extern bool TrackMouseEvent(ref TRACKMOUSEEVENT lpEventTrack); + + [StructLayout(LayoutKind.Sequential)] + public struct TRACKMOUSEEVENT + { + public int cbSize; + public uint dwFlags; + public IntPtr hwndTrack; + public uint dwHoverTime; + } } diff --git a/samples/IntegrationTestApp/Pages/EmbeddingPage.axaml b/samples/IntegrationTestApp/Pages/EmbeddingPage.axaml index a2011d6636..632893bcd0 100644 --- a/samples/IntegrationTestApp/Pages/EmbeddingPage.axaml +++ b/samples/IntegrationTestApp/Pages/EmbeddingPage.axaml @@ -15,5 +15,6 @@ + diff --git a/samples/IntegrationTestApp/Pages/EmbeddingPage.axaml.cs b/samples/IntegrationTestApp/Pages/EmbeddingPage.axaml.cs index 5e1fa8c517..93855cd13d 100644 --- a/samples/IntegrationTestApp/Pages/EmbeddingPage.axaml.cs +++ b/samples/IntegrationTestApp/Pages/EmbeddingPage.axaml.cs @@ -1,4 +1,5 @@ using Avalonia.Controls; +using Avalonia.Interactivity; namespace IntegrationTestApp; @@ -7,5 +8,16 @@ public partial class EmbeddingPage : UserControl public EmbeddingPage() { InitializeComponent(); + ResetText(); + } + + private void ResetText() + { + NativeTextBox.Text = NativeTextBoxInPopup.Text = "Native text box"; + } + + private void Reset_Click(object? sender, RoutedEventArgs e) + { + ResetText(); } } diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 940528bf6f..e8c36546ba 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -327,6 +327,7 @@ namespace Avalonia.Controls { IsLightDismissEnabled = true, OverlayDismissEventPassThrough = true, + TakesFocusFromNativeControl = Popup.GetTakesFocusFromNativeControl(this), }; _popup.Opened += PopupOpened; diff --git a/src/Avalonia.Controls/Platform/IPopupImpl.cs b/src/Avalonia.Controls/Platform/IPopupImpl.cs index 320130bc91..388c447243 100644 --- a/src/Avalonia.Controls/Platform/IPopupImpl.cs +++ b/src/Avalonia.Controls/Platform/IPopupImpl.cs @@ -12,5 +12,6 @@ namespace Avalonia.Platform IPopupPositioner? PopupPositioner { get; } void SetWindowManagerAddShadowHint(bool enabled); + void TakeFocus(); } } diff --git a/src/Avalonia.Controls/Primitives/IPopupHost.cs b/src/Avalonia.Controls/Primitives/IPopupHost.cs index 4cb259db5f..c4f992363e 100644 --- a/src/Avalonia.Controls/Primitives/IPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/IPopupHost.cs @@ -98,5 +98,10 @@ namespace Avalonia.Controls.Primitives /// Hides the popup. /// void Hide(); + + /// + /// Takes focus from any currently focused native control. + /// + void TakeFocus(); } } diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index 3a602c15b7..3ea7ac5451 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -73,6 +73,11 @@ namespace Avalonia.Controls.Primitives _shown = false; } + public void TakeFocus() + { + // Nothing to do here: overlay popups are implemented inside the window. + } + /// [Unstable(ObsoletionMessages.MayBeRemovedInAvalonia12)] public void ConfigurePosition(Visual target, PlacementMode placement, Point offset, diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 28e2a51e06..87962846aa 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -134,6 +134,12 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty TopmostProperty = AvaloniaProperty.Register(nameof(Topmost)); + /// + /// Defines the property. + /// + public static readonly AttachedProperty TakesFocusFromNativeControlProperty = + AvaloniaProperty.RegisterAttached(nameof(TakesFocusFromNativeControl), true); + private bool _isOpenRequested; private bool _ignoreIsOpenChanged; private PopupOpenState? _openState; @@ -364,6 +370,23 @@ namespace Avalonia.Controls.Primitives set => SetValue(TopmostProperty, value); } + /// + /// Gets or sets a value indicating whether the popup, on show, transfers focus from any + /// focused native control to Avalonia. The default is true. + /// + /// + /// This property only applies to advanced native control embedding scenarios. By default, + /// if a popup is shown when a native control is focused, focus is transferred back to + /// Avalonia in order for the popup to receive input. If this property is set to + /// false, then the shown popup will not receive input until it receives an + /// interaction which explicitly focuses the popup, such as a mouse click. + /// + public bool TakesFocusFromNativeControl + { + get => GetValue(TakesFocusFromNativeControlProperty); + set => SetValue(TakesFocusFromNativeControlProperty, value); + } + IPopupHost? IPopupHostProvider.PopupHost => Host; event Action? IPopupHostProvider.PopupHostChanged @@ -520,6 +543,9 @@ namespace Avalonia.Controls.Primitives popupHost.Show(); + if (TakesFocusFromNativeControl) + popupHost.TakeFocus(); + using (BeginIgnoringIsOpen()) { SetCurrentValue(IsOpenProperty, true); @@ -535,6 +561,27 @@ namespace Avalonia.Controls.Primitives /// public void Close() => CloseCore(); + /// + /// Gets the value of the attached property on the + /// specified control. + /// + /// The control. + public static bool GetTakesFocusFromNativeControl(Control control) + { + return control.GetValue(TakesFocusFromNativeControlProperty); + } + + /// + /// Sets the value of the attached property on the + /// specified control. + /// + /// The control. + /// The value of the TakesFocusFromNativeControl property. + public static void SetTakesFocusFromNativeControl(Control control, bool value) + { + control.SetValue(TakesFocusFromNativeControlProperty, value); + } + /// /// Measures the control. /// diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index b2905b9176..f88ed0b93b 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -156,6 +156,8 @@ namespace Avalonia.Controls.Primitives public void SetChild(Control? control) => Content = control; + public void TakeFocus() => PlatformImpl?.TakeFocus(); + Visual IPopupHost.HostedVisualTreeRoot => this; protected override Size MeasureOverride(Size availableSize) diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index fc8da13131..d2b3da81fe 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -408,6 +408,7 @@ namespace Avalonia.Controls { _popup = new Popup(); _popup.Child = this; + _popup.TakesFocusFromNativeControl = false; _popup.WindowManagerAddShadowHint = false; _popup.Opened += OnPopupOpened; diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index e21f4dc843..de8f0749e1 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -198,6 +198,7 @@ namespace Avalonia.DesignerSupport.Remote public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); public object TryGetFeature(Type featureType) => null; + public void TakeFocus() { } } class ClipboardStub : IClipboard diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index 14383c93c8..222fa63497 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -87,6 +87,22 @@ namespace Avalonia.Native { } + public void TakeFocus() + { + var parent = _parent; + + while (parent != null) + { + if (parent is PopupImpl popup) + parent = popup._parent; + else + break; + } + + if (parent is WindowImpl w) + w.Native.TakeFocusFromChildren(); + } + public IPopupPositioner PopupPositioner { get; } } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 6c1b381a40..73904d5d63 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -1562,5 +1562,10 @@ namespace Avalonia.X11 } } } + + public void TakeFocus() + { + // TODO: Not yet implemented: need to check if this is required on X11 or not. + } } } diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs index 5ff9b8d1ae..7366d8235d 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs @@ -443,5 +443,9 @@ namespace Avalonia.Headless zOrder[i] = headlessWindowImpl._zOrder; } } + + public void TakeFocus() + { + } } } diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index 388ee505e1..95c8178adc 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -22,6 +22,7 @@ namespace Avalonia.Win32 { // Popups are always shown non-activated. UnmanagedMethods.ShowWindow(Handle.Handle, UnmanagedMethods.ShowWindowCommand.ShowNoActivate); + } protected override bool ShouldTakeFocusOnClick => false; @@ -147,6 +148,28 @@ namespace Avalonia.Win32 EnableBoxShadow(Handle.Handle, enabled); } + public void TakeFocus() + { + var parent = _parent; + + while (parent != null) + { + if (parent is PopupImpl pi) + parent = pi._parent; + else + break; + } + + if (parent == null) + return; + + var focusOwner = UnmanagedMethods.GetFocus(); + if (focusOwner != IntPtr.Zero && + UnmanagedMethods.GetAncestor(focusOwner, UnmanagedMethods.GetAncestorFlags.GA_ROOT) + == parent.Handle?.Handle) + UnmanagedMethods.SetFocus(parent.Handle.Handle); + } + public IPopupPositioner PopupPositioner { get; } } } diff --git a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs index d9317e8e6a..18e9912faf 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs @@ -136,7 +136,7 @@ namespace Avalonia.IntegrationTests.Appium /// /// An object which when disposed will cause the newly opened window to close. /// - public static IDisposable OpenWindowWithClick(this AppiumWebElement element) + public static IDisposable OpenWindowWithClick(this AppiumWebElement element, TimeSpan? delay = null) { var session = element.WrappedDriver; @@ -148,6 +148,9 @@ namespace Avalonia.IntegrationTests.Appium element.Click(); + if (delay is not null) + Thread.Sleep((int)delay.Value.TotalMilliseconds); + var newHandle = session.WindowHandles.Except(oldHandles).SingleOrDefault(); if (newHandle is not null) @@ -167,6 +170,7 @@ namespace Avalonia.IntegrationTests.Appium // that a child window was opened. These don't appear in session.WindowHandles // so we have to use an XPath query to get hold of it. var newChildWindows = session.FindElements(By.XPath("//Window")); + var pageSource = session.PageSource; var childWindow = Assert.Single(newChildWindows.Except(oldChildWindows)); return Disposable.Create(() => diff --git a/tests/Avalonia.IntegrationTests.Appium/EmbeddingTests.cs b/tests/Avalonia.IntegrationTests.Appium/EmbeddingTests.cs index 15bb5cda62..f4debf8fe3 100644 --- a/tests/Avalonia.IntegrationTests.Appium/EmbeddingTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/EmbeddingTests.cs @@ -1,4 +1,7 @@ using System; +using System.Threading; +using OpenQA.Selenium; +using OpenQA.Selenium.Interactions; using Xunit; namespace Avalonia.IntegrationTests.Appium @@ -9,6 +12,8 @@ namespace Avalonia.IntegrationTests.Appium public EmbeddingTests(DefaultAppFixture fixture) : base(fixture, "Embedding") { + var reset = Session.FindElementByAccessibilityId("Reset"); + reset.Click(); } [PlatformFact(TestPlatforms.Windows, "Not yet working on macOS")] @@ -60,5 +65,56 @@ namespace Avalonia.IntegrationTests.Appium checkBox.Click(); } } + + [PlatformFact(TestPlatforms.Windows, "Not yet working on macOS")] + public void Showing_ToolTip_Does_Not_Steal_Focus_From_Native_TextBox() + { + // Appium has different XPath syntax between Windows and macOS. + var textBox = OperatingSystem.IsWindows() ? + Session.FindElementByXPath($"//*[@AutomationId='NativeTextBox']//*[1]") : + Session.FindElementByXPath($"//*[@identifier='NativeTextBox']//*[1]"); + + // Clicking on the text box causes the cursor to hover over it, opening the tooltip. + textBox.Click(); + Thread.Sleep(1000); + + // Ensure the tooltip has opened. + Session.FindElementByAccessibilityId("NativeTextBoxToolTip"); + + // The tooltip should not have stolen focus from the text box, so text entry should work. + new Actions(Session).SendKeys("Hello world!").Perform(); + + // SendKeys behaves differently between Windows and macOS. On Windows it inserts at the start + // of the text box, on macOS it replaces the text for some reason. Sigh. + var expected = OperatingSystem.IsWindows() ? + "Native text boxHello world!" : + "Hello world!"; + + Assert.Equal(expected, textBox.Text); + } + + [PlatformFact(TestPlatforms.Windows, "Not yet working on macOS")] + public void Showing_ContextMenu_Steals_Focus_From_Native_TextBox() + { + // Appium has different XPath syntax between Windows and macOS. + var textBox = OperatingSystem.IsWindows() ? + Session.FindElementByXPath($"//*[@AutomationId='NativeTextBox']//*[1]") : + Session.FindElementByXPath($"//*[@identifier='NativeTextBox']//*[1]"); + + // Click on the text box the right-click to show the context menu. + textBox.Click(); + new Actions(Session).ContextClick(textBox).Perform(); + + // Ensure the context menu has opened. + Session.FindElementByAccessibilityId("NativeTextBoxContextMenu"); + + // Select the first menu item with the keyboard. + new Actions(Session) + .SendKeys(Keys.ArrowDown) + .SendKeys(Keys.Enter) + .Perform(); + + Assert.Equal("Context menu item clicked", textBox.Text); + } } }