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);
+ }
}
}