diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index 543aa78cbe..7171de38f7 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -13,7 +13,7 @@ NSMutableArray* _children; } -+ (id _Nullable)acquire:(IAvnAutomationPeer *)peer ++ (NSAccessibilityElement *)acquire:(IAvnAutomationPeer *)peer { if (peer == nullptr) return nil; @@ -23,7 +23,12 @@ if (instance != nullptr) return dynamic_cast(instance)->GetOwner(); - if (peer->IsRootProvider()) + if (peer->IsInteropPeer()) + { + auto view = (__bridge NSAccessibilityElement*)peer->InteropPeer_GetNativeControlHandle(); + return view; + } + else if (peer->IsRootProvider()) { auto window = peer->RootProvider_GetWindow(); @@ -35,7 +40,7 @@ auto holder = dynamic_cast(window); auto view = holder->GetNSView(); - return [view window]; + return (NSAccessibilityElement*)[view window]; } else { diff --git a/samples/IntegrationTestApp/Embedding/INativeControlFactory.cs b/samples/IntegrationTestApp/Embedding/INativeControlFactory.cs new file mode 100644 index 0000000000..4e6b290f48 --- /dev/null +++ b/samples/IntegrationTestApp/Embedding/INativeControlFactory.cs @@ -0,0 +1,9 @@ +using System; +using Avalonia.Platform; + +namespace IntegrationTestApp.Embedding; + +internal interface INativeControlFactory +{ + IPlatformHandle CreateControl(IPlatformHandle parent, Func createDefault); +} diff --git a/samples/IntegrationTestApp/Embedding/MacHelper.cs b/samples/IntegrationTestApp/Embedding/MacHelper.cs new file mode 100644 index 0000000000..9123a5f49e --- /dev/null +++ b/samples/IntegrationTestApp/Embedding/MacHelper.cs @@ -0,0 +1,16 @@ +using MonoMac.AppKit; + +namespace IntegrationTestApp.Embedding; + +internal class MacHelper +{ + private static bool s_isInitialized; + + public static void EnsureInitialized() + { + if (s_isInitialized) + return; + s_isInitialized = true; + NSApplication.Init(); + } +} diff --git a/samples/IntegrationTestApp/Embedding/MacOSTextBoxFactory.cs b/samples/IntegrationTestApp/Embedding/MacOSTextBoxFactory.cs new file mode 100644 index 0000000000..c236055ce9 --- /dev/null +++ b/samples/IntegrationTestApp/Embedding/MacOSTextBoxFactory.cs @@ -0,0 +1,20 @@ +using System; +using System.Text; +using Avalonia.Platform; +using MonoMac.AppKit; +using MonoMac.WebKit; + +namespace IntegrationTestApp.Embedding; + +internal class MacOSTextBoxFactory : INativeControlFactory +{ + public IPlatformHandle CreateControl(IPlatformHandle parent, Func createDefault) + { + MacHelper.EnsureInitialized(); + + var textView = new NSTextView(); + textView.TextStorage.Append(new("Native text box")); + + return new MacOSViewHandle(textView); + } +} diff --git a/samples/IntegrationTestApp/Embedding/MacOSViewHandle.cs b/samples/IntegrationTestApp/Embedding/MacOSViewHandle.cs new file mode 100644 index 0000000000..b6cadcce23 --- /dev/null +++ b/samples/IntegrationTestApp/Embedding/MacOSViewHandle.cs @@ -0,0 +1,19 @@ +using System; +using Avalonia.Controls.Platform; +using MonoMac.AppKit; + +namespace IntegrationTestApp.Embedding; + +internal class MacOSViewHandle(NSView view) : INativeControlHostDestroyableControlHandle +{ + private NSView? _view = view; + + public IntPtr Handle => _view?.Handle ?? IntPtr.Zero; + public string HandleDescriptor => "NSView"; + + public void Destroy() + { + _view?.Dispose(); + _view = null; + } +} diff --git a/samples/IntegrationTestApp/Embedding/NativeTextBox.cs b/samples/IntegrationTestApp/Embedding/NativeTextBox.cs new file mode 100644 index 0000000000..8bbc6dc560 --- /dev/null +++ b/samples/IntegrationTestApp/Embedding/NativeTextBox.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; +using Avalonia.Platform; + +namespace IntegrationTestApp.Embedding; + +internal class NativeTextBox : NativeControlHost +{ + public static INativeControlFactory? Factory { get; set; } + + protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent) + { + return Factory?.CreateControl(parent, () => base.CreateNativeControlCore(parent)) + ?? base.CreateNativeControlCore(parent); + } + + protected override void DestroyNativeControlCore(IPlatformHandle control) + { + base.DestroyNativeControlCore(control); + } +} diff --git a/samples/IntegrationTestApp/Embedding/Win32TextBoxFactory.cs b/samples/IntegrationTestApp/Embedding/Win32TextBoxFactory.cs new file mode 100644 index 0000000000..e29699de3f --- /dev/null +++ b/samples/IntegrationTestApp/Embedding/Win32TextBoxFactory.cs @@ -0,0 +1,21 @@ +using System; +using System.Text; +using Avalonia.Platform; + +namespace IntegrationTestApp.Embedding; + +internal class Win32TextBoxFactory : INativeControlFactory +{ + public IPlatformHandle CreateControl(IPlatformHandle parent, Func createDefault) + { + 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"); + } +} diff --git a/samples/IntegrationTestApp/Embedding/Win32WindowControlHandle.cs b/samples/IntegrationTestApp/Embedding/Win32WindowControlHandle.cs new file mode 100644 index 0000000000..ec347f4f09 --- /dev/null +++ b/samples/IntegrationTestApp/Embedding/Win32WindowControlHandle.cs @@ -0,0 +1,11 @@ +using System; +using Avalonia.Controls.Platform; +using Avalonia.Platform; + +namespace IntegrationTestApp.Embedding; + +internal class Win32WindowControlHandle : PlatformHandle, INativeControlHostDestroyableControlHandle +{ + public Win32WindowControlHandle(IntPtr handle, string descriptor) : base(handle, descriptor) { } + public void Destroy() => WinApi.DestroyWindow(Handle); +} diff --git a/samples/IntegrationTestApp/Embedding/WinApi.cs b/samples/IntegrationTestApp/Embedding/WinApi.cs new file mode 100644 index 0000000000..ab51988df9 --- /dev/null +++ b/samples/IntegrationTestApp/Embedding/WinApi.cs @@ -0,0 +1,82 @@ +using System; +using System.Runtime.InteropServices; + +namespace IntegrationTestApp.Embedding; + +internal class WinApi +{ + [Flags] + public enum WindowStyles : uint + { + WS_BORDER = 0x800000, + WS_CAPTION = 0xc00000, + WS_CHILD = 0x40000000, + WS_CLIPCHILDREN = 0x2000000, + WS_CLIPSIBLINGS = 0x4000000, + WS_DISABLED = 0x8000000, + WS_DLGFRAME = 0x400000, + WS_GROUP = 0x20000, + WS_HSCROLL = 0x100000, + WS_MAXIMIZE = 0x1000000, + WS_MAXIMIZEBOX = 0x10000, + WS_MINIMIZE = 0x20000000, + WS_MINIMIZEBOX = 0x20000, + WS_OVERLAPPED = 0x0, + WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX, + WS_POPUP = 0x80000000u, + WS_POPUPWINDOW = WS_POPUP | WS_BORDER | WS_SYSMENU, + WS_SYSMENU = 0x80000, + WS_TABSTOP = 0x10000, + WS_THICKFRAME = 0x40000, + WS_VISIBLE = 0x10000000, + WS_VSCROLL = 0x200000, + WS_EX_DLGMODALFRAME = 0x00000001, + WS_EX_NOPARENTNOTIFY = 0x00000004, + WS_EX_NOREDIRECTIONBITMAP = 0x00200000, + WS_EX_TOPMOST = 0x00000008, + WS_EX_ACCEPTFILES = 0x00000010, + WS_EX_TRANSPARENT = 0x00000020, + WS_EX_MDICHILD = 0x00000040, + WS_EX_TOOLWINDOW = 0x00000080, + WS_EX_WINDOWEDGE = 0x00000100, + WS_EX_CLIENTEDGE = 0x00000200, + WS_EX_CONTEXTHELP = 0x00000400, + WS_EX_RIGHT = 0x00001000, + WS_EX_LEFT = 0x00000000, + WS_EX_RTLREADING = 0x00002000, + WS_EX_LTRREADING = 0x00000000, + WS_EX_LEFTSCROLLBAR = 0x00004000, + WS_EX_RIGHTSCROLLBAR = 0x00000000, + WS_EX_CONTROLPARENT = 0x00010000, + WS_EX_STATICEDGE = 0x00020000, + WS_EX_APPWINDOW = 0x00040000, + WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE, + WS_EX_PALETTEWINDOW = WS_EX_WINDOWEDGE | WS_EX_TOOLWINDOW | WS_EX_TOPMOST, + WS_EX_LAYERED = 0x00080000, + WS_EX_NOINHERITLAYOUT = 0x00100000, + WS_EX_LAYOUTRTL = 0x00400000, + WS_EX_COMPOSITED = 0x02000000, + WS_EX_NOACTIVATE = 0x08000000 + } + + [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", SetLastError = true)] + public static extern IntPtr CreateWindowEx( + int dwExStyle, + string lpClassName, + string lpWindowName, + uint dwStyle, + int x, + int y, + int nWidth, + int nHeight, + IntPtr hWndParent, + IntPtr hMenu, + IntPtr hInstance, + IntPtr lpParam); +} diff --git a/samples/IntegrationTestApp/IntegrationTestApp.csproj b/samples/IntegrationTestApp/IntegrationTestApp.csproj index 517537826e..7d23408edd 100644 --- a/samples/IntegrationTestApp/IntegrationTestApp.csproj +++ b/samples/IntegrationTestApp/IntegrationTestApp.csproj @@ -4,6 +4,7 @@ $(AvsCurrentTargetFramework) enable $(NoWarn);AVP1012 + app.manifest true @@ -13,13 +14,14 @@ true 1.0.0 - + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 9a69563a77..8eac3a5886 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -63,6 +63,7 @@ namespace IntegrationTestApp new("ComboBox", () => new ComboBoxPage()), new("ContextMenu", () => new ContextMenuPage()), new("DesktopPage", () => new DesktopPage()), + new("Embedding", () => new EmbeddingPage()), new("Gestures", () => new GesturesPage()), new("ListBox", () => new ListBoxPage()), new("Menu", () => new MenuPage()), diff --git a/samples/IntegrationTestApp/Pages/EmbeddingPage.axaml b/samples/IntegrationTestApp/Pages/EmbeddingPage.axaml new file mode 100644 index 0000000000..a2011d6636 --- /dev/null +++ b/samples/IntegrationTestApp/Pages/EmbeddingPage.axaml @@ -0,0 +1,19 @@ + + + + + Open Popup + + + + + + diff --git a/samples/IntegrationTestApp/Pages/EmbeddingPage.axaml.cs b/samples/IntegrationTestApp/Pages/EmbeddingPage.axaml.cs new file mode 100644 index 0000000000..5e1fa8c517 --- /dev/null +++ b/samples/IntegrationTestApp/Pages/EmbeddingPage.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace IntegrationTestApp; + +public partial class EmbeddingPage : UserControl +{ + public EmbeddingPage() + { + InitializeComponent(); + } +} diff --git a/samples/IntegrationTestApp/Program.cs b/samples/IntegrationTestApp/Program.cs index 6603450b85..43c936bb1c 100644 --- a/samples/IntegrationTestApp/Program.cs +++ b/samples/IntegrationTestApp/Program.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Avalonia; +using IntegrationTestApp.Embedding; namespace IntegrationTestApp { @@ -31,6 +32,13 @@ namespace IntegrationTestApp public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() + .AfterSetup(builder => + { + NativeTextBox.Factory = + OperatingSystem.IsWindows() ? new Win32TextBoxFactory() : + OperatingSystem.IsMacOS() ? new MacOSTextBoxFactory() : + null; + }) .LogToTrace(); } } diff --git a/samples/IntegrationTestApp/app.manifest b/samples/IntegrationTestApp/app.manifest new file mode 100644 index 0000000000..db90057191 --- /dev/null +++ b/samples/IntegrationTestApp/app.manifest @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls/Automation/Peers/InteropAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/InteropAutomationPeer.cs new file mode 100644 index 0000000000..367c7804c3 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/InteropAutomationPeer.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using Avalonia.Automation.Peers; +using Avalonia.Platform; + +namespace Avalonia.Controls.Automation.Peers; + +/// +/// Represents the root of a native control automation tree hosted by a . +/// +/// +/// This peer should be special-cased in the platform backend, as it represents a native control +/// and hence none of the standard automation peer methods are applicable. +/// +internal class InteropAutomationPeer : AutomationPeer +{ + private AutomationPeer? _parent; + + public InteropAutomationPeer(IPlatformHandle nativeControlHandle) => NativeControlHandle = nativeControlHandle; + public IPlatformHandle NativeControlHandle { get; } + + protected override void BringIntoViewCore() => throw new NotImplementedException(); + protected override string? GetAcceleratorKeyCore() => throw new NotImplementedException(); + protected override string? GetAccessKeyCore() => throw new NotImplementedException(); + protected override AutomationControlType GetAutomationControlTypeCore() => throw new NotImplementedException(); + protected override string? GetAutomationIdCore() => throw new NotImplementedException(); + protected override Rect GetBoundingRectangleCore() => throw new NotImplementedException(); + protected override string GetClassNameCore() => throw new NotImplementedException(); + protected override AutomationPeer? GetLabeledByCore() => throw new NotImplementedException(); + protected override string? GetNameCore() => throw new NotImplementedException(); + protected override IReadOnlyList GetOrCreateChildrenCore() => throw new NotImplementedException(); + protected override AutomationPeer? GetParentCore() => _parent; + protected override bool HasKeyboardFocusCore() => throw new NotImplementedException(); + protected override bool IsContentElementCore() => throw new NotImplementedException(); + protected override bool IsControlElementCore() => throw new NotImplementedException(); + protected override bool IsEnabledCore() => throw new NotImplementedException(); + protected override bool IsKeyboardFocusableCore() => throw new NotImplementedException(); + protected override void SetFocusCore() => throw new NotImplementedException(); + protected override bool ShowContextMenuCore() => throw new NotImplementedException(); + + protected internal override bool TrySetParent(AutomationPeer? parent) + { + _parent = parent; + return true; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/NativeControlHostPeer.cs b/src/Avalonia.Controls/Automation/Peers/NativeControlHostPeer.cs new file mode 100644 index 0000000000..5f3ea30cb6 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/NativeControlHostPeer.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using Avalonia.Automation.Peers; + +namespace Avalonia.Controls.Automation.Peers; + +internal class NativeControlHostPeer : ControlAutomationPeer +{ + public NativeControlHostPeer(NativeControlHost owner) + : base(owner) + { + owner.NativeControlHandleChanged += OnNativeControlHandleChanged; + } + + protected override IReadOnlyList? GetChildrenCore() + { + if (Owner is NativeControlHost host && host.NativeControlHandle != null) + return [new InteropAutomationPeer(host.NativeControlHandle)]; + return null; + } + + private void OnNativeControlHandleChanged(object? sender, EventArgs e) => InvalidateChildren(); +} diff --git a/src/Avalonia.Controls/NativeControlHost.cs b/src/Avalonia.Controls/NativeControlHost.cs index 3a49bcceb0..a6ad90dfcb 100644 --- a/src/Avalonia.Controls/NativeControlHost.cs +++ b/src/Avalonia.Controls/NativeControlHost.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Automation.Peers; using Avalonia.Controls.Platform; -using Avalonia.Media; using Avalonia.Platform; using Avalonia.Threading; using Avalonia.VisualTree; @@ -24,6 +25,21 @@ namespace Avalonia.Controls FlowDirectionProperty.Changed.AddClassHandler(OnFlowDirectionChanged); } + internal IPlatformHandle? NativeControlHandle + { + get => _nativeControlHandle; + set + { + if (_nativeControlHandle != value) + { + _nativeControlHandle = value; + NativeControlHandleChanged?.Invoke(this, EventArgs.Empty); + } + } + } + + internal event EventHandler? NativeControlHandleChanged; + /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { @@ -89,19 +105,19 @@ namespace Avalonia.Controls // If there is no attachment, but the control exists, // attempt to attach to the current toplevel or destroy the control if it's incompatible - if (_attachment == null && _nativeControlHandle != null) + if (_attachment == null && NativeControlHandle != null) { - if (_currentHost.IsCompatibleWith(_nativeControlHandle)) - _attachment = _currentHost.CreateNewAttachment(_nativeControlHandle); + if (_currentHost.IsCompatibleWith(NativeControlHandle)) + _attachment = _currentHost.CreateNewAttachment(NativeControlHandle); else DestroyNativeControl(); } // There is no control handle an no attachment, create both - if (_nativeControlHandle == null) + if (NativeControlHandle == null) { _attachment = _currentHost.CreateNewAttachment(parent => - _nativeControlHandle = CreateNativeControlCore(parent)); + NativeControlHandle = CreateNativeControlCore(parent)); } } else @@ -111,7 +127,7 @@ namespace Avalonia.Controls _attachment.AttachedTo = null; // Don't destroy the control immediately, it might be just being reparented to another TopLevel - if (_nativeControlHandle != null && !_queuedForDestruction) + if (NativeControlHandle != null && !_queuedForDestruction) { _queuedForDestruction = true; Dispatcher.UIThread.Post(CheckDestruction, DispatcherPriority.Background); @@ -180,13 +196,13 @@ namespace Avalonia.Controls private void DestroyNativeControl() { - if (_nativeControlHandle != null) + if (NativeControlHandle != null) { _attachment?.Dispose(); _attachment = null; - DestroyNativeControlCore(_nativeControlHandle); - _nativeControlHandle = null; + DestroyNativeControlCore(NativeControlHandle); + NativeControlHandle = null; } } @@ -197,6 +213,7 @@ namespace Avalonia.Controls nativeControlHostDestroyableControlHandle.Destroy(); } } - + + protected override AutomationPeer OnCreateAutomationPeer() => new NativeControlHostPeer(this); } } diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index af4958b02f..d62cb04130 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -7,6 +7,7 @@ using Avalonia.Automation; using Avalonia.Automation.Peers; using Avalonia.Automation.Provider; using Avalonia.Controls; +using Avalonia.Controls.Automation.Peers; using Avalonia.Native.Interop; #nullable enable @@ -56,6 +57,9 @@ namespace Avalonia.Native Node = node; } + public int IsInteropPeer() => (_inner is InteropAutomationPeer).AsComBool(); + public IntPtr InteropPeer_GetNativeControlHandle() => ((InteropAutomationPeer)_inner).NativeControlHandle.Handle; + public IAvnAutomationPeer? RootPeer { get diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index aa2e52449d..add248be29 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -1180,6 +1180,9 @@ interface IAvnAutomationPeer : IUnknown IAvnAutomationPeer* GetRootPeer(); + bool IsInteropPeer(); + [intptr]void* InteropPeer_GetNativeControlHandle(); + bool IsRootProvider(); IAvnWindowBase* RootProvider_GetWindow(); IAvnAutomationPeer* RootProvider_GetFocus(); diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index 0b3e707acc..0a6f4d8d8e 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -8,6 +8,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Avalonia.Automation; using Avalonia.Automation.Peers; +using Avalonia.Controls.Automation.Peers; using Avalonia.Threading; using Avalonia.Win32.Interop.Automation; using AAP = Avalonia.Automation.Provider; @@ -63,7 +64,7 @@ namespace Avalonia.Win32.Automation public AutomationPeer Peer { get; protected set; } - public Rect BoundingRectangle + public virtual Rect BoundingRectangle { get => InvokeSync(() => { @@ -79,7 +80,7 @@ namespace Avalonia.Win32.Automation } public virtual IRawElementProviderSimple? HostRawElementProvider => null; - public ProviderOptions ProviderOptions => ProviderOptions.ServerSideProvider; + public virtual ProviderOptions ProviderOptions => ProviderOptions.ServerSideProvider; [return: MarshalAs(UnmanagedType.IUnknown)] public virtual object? GetPatternProvider(int patternId) @@ -275,9 +276,12 @@ namespace Avalonia.Win32.Automation private static AutomationNode Create(AutomationPeer peer) { - return peer.GetProvider() is object ? - new RootAutomationNode(peer) : - new AutomationNode(peer); + if (peer is InteropAutomationPeer interop) + return new InteropAutomationNode(interop); + else if (peer.GetProvider() is not null) + return new RootAutomationNode(peer); + else + return new AutomationNode(peer); } private static UiaControlTypeId ToUiaControlType(AutomationControlType role) diff --git a/src/Windows/Avalonia.Win32/Automation/InteropAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/InteropAutomationNode.cs new file mode 100644 index 0000000000..9ef8b79cfe --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/InteropAutomationNode.cs @@ -0,0 +1,48 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using Avalonia.Controls.Automation.Peers; +using Avalonia.Win32.Interop.Automation; + +namespace Avalonia.Win32.Automation; + +/// +/// An automation node which serves as the root of an embedded native control automation tree. +/// +[RequiresUnreferencedCode("Requires .NET COM interop")] +internal class InteropAutomationNode : AutomationNode, IRawElementProviderFragmentRoot +{ + private readonly IntPtr _handle; + + public InteropAutomationNode(InteropAutomationPeer peer) + : base(peer) + { + _handle = peer.NativeControlHandle.Handle; + } + + public override Rect BoundingRectangle => default; + public override IRawElementProviderFragmentRoot? FragmentRoot => null; + public override ProviderOptions ProviderOptions => ProviderOptions.ServerSideProvider | ProviderOptions.OverrideProvider; + + public override object? GetPatternProvider(int patternId) => null; + public override object? GetPropertyValue(int propertyId) => null; + + public override IRawElementProviderSimple? HostRawElementProvider + { + get + { + var hr = UiaCoreProviderApi.UiaHostProviderFromHwnd(_handle, out var result); + Marshal.ThrowExceptionForHR(hr); + return result; + } + } + + public override IRawElementProviderFragment? Navigate(NavigateDirection direction) + { + return direction == NavigateDirection.Parent ? base.Navigate(direction) : null; + } + + public IRawElementProviderFragment? ElementProviderFromPoint(double x, double y) => null; + public IRawElementProviderFragment? GetFocus() => null; + public IRawElementProviderSimple[]? GetEmbeddedFragmentRoots() => null; +} diff --git a/tests/Avalonia.IntegrationTests.Appium/EmbeddingTests.cs b/tests/Avalonia.IntegrationTests.Appium/EmbeddingTests.cs new file mode 100644 index 0000000000..15bb5cda62 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/EmbeddingTests.cs @@ -0,0 +1,64 @@ +using System; +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + [Collection("Default")] + public class EmbeddingTests : TestBase + { + public EmbeddingTests(DefaultAppFixture fixture) + : base(fixture, "Embedding") + { + } + + [PlatformFact(TestPlatforms.Windows, "Not yet working on macOS")] + public void Can_Edit_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]"); + + Assert.Equal("Native text box", textBox.Text); + + textBox.SendKeys("Hello world!"); + + // 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() ? + "Hello world!Native text box" : + "Hello world!"; + Assert.Equal(expected, textBox.Text); + } + + [PlatformFact(TestPlatforms.Windows, "Not yet working on macOS")] + public void Can_Edit_Native_TextBox_In_Popup() + { + var checkBox = Session.FindElementByAccessibilityId("EmbeddingPopupOpenCheckBox"); + checkBox.Click(); + + try + { + // Appium has different XPath syntax between Windows and macOS. + var textBox = OperatingSystem.IsWindows() ? + Session.FindElementByXPath($"//*[@AutomationId='NativeTextBoxInPopup']//*[1]") : + Session.FindElementByXPath($"//*[@identifier='NativeTextBoxInPopup']//*[1]"); + + Assert.Equal("Native text box", textBox.Text); + + textBox.SendKeys("Hello world!"); + + // 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() ? + "Hello world!Native text box" : + "Hello world!"; + Assert.Equal(expected, textBox.Text); + } + finally + { + checkBox.Click(); + } + } + } +}