зеркало из https://github.com/AvaloniaUI/Avalonia.git
Add basic integration tests for NativeControlHost and improve its automation/a11y support. (#15542)
* Added embedding page to IntegrationTestApp. Currently embeds a lone native text box, and only when running on Windows. * Win32 automation support for native control host. Allows native controls to appear in the Avalonia automation tree as a child of the `NativeControlHost`. They also appear in the _wrong_ place - as a direct child of the `Window` - but this appears to be expected behavior as it happens when hosting a win32 control in WPF as well. * Basic native control integration test on win32. * Test editing native win32 control in popup. * Add embedded text box on macOS. * macOS automation support for native control host. Implements special-casing of `InteropAutomationPeer`on macOS. * Make native control integration test work on macOS. The test for the native control in a popup is disabled on macOS because we have a bug there. * Add missing parts * Fix build error * Skip test to see if CI passes again. * Log more info about integration tests on win32. * Try to fix flaky test. * The tests won't yet work on macOS yet. Will require #16577. --------- Co-authored-by: Benedikt Stebner <Gillibald@users.noreply.github.com> Co-authored-by: Max Katz <maxkatz6@outlook.com>
This commit is contained in:
Родитель
d1cdb29ba0
Коммит
c6cdbfec3b
|
@ -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<AvnAutomationNode*>(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<INSViewHolder*>(window);
|
||||
auto view = holder->GetNSView();
|
||||
return [view window];
|
||||
return (NSAccessibilityElement*)[view window];
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
using System;
|
||||
using Avalonia.Platform;
|
||||
|
||||
namespace IntegrationTestApp.Embedding;
|
||||
|
||||
internal interface INativeControlFactory
|
||||
{
|
||||
IPlatformHandle CreateControl(IPlatformHandle parent, Func<IPlatformHandle> createDefault);
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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<IPlatformHandle> createDefault)
|
||||
{
|
||||
MacHelper.EnsureInitialized();
|
||||
|
||||
var textView = new NSTextView();
|
||||
textView.TextStorage.Append(new("Native text box"));
|
||||
|
||||
return new MacOSViewHandle(textView);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<IPlatformHandle> 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");
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
<TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<NoWarn>$(NoWarn);AVP1012</NoWarn>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
|
||||
</PropertyGroup>
|
||||
|
||||
|
@ -13,13 +14,14 @@
|
|||
<NSHighResolutionCapable>true</NSHighResolutionCapable>
|
||||
<CFBundleShortVersionString>1.0.0</CFBundleShortVersionString>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dotnet.Bundle" Version="0.9.13" />
|
||||
<PackageReference Include="MonoMac.NetStandard" Version="0.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:embedding="using:IntegrationTestApp.Embedding"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="IntegrationTestApp.EmbeddingPage">
|
||||
<StackPanel>
|
||||
<embedding:NativeTextBox Name="NativeTextBox" Height="23"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<CheckBox Name="EmbeddingPopupOpenCheckBox">Open Popup</CheckBox>
|
||||
<Popup IsOpen="{Binding #EmbeddingPopupOpenCheckBox.IsChecked}"
|
||||
PlacementTarget="EmbeddingPopupOpenCheckBox"
|
||||
Placement="Right">
|
||||
<embedding:NativeTextBox Name="NativeTextBoxInPopup" Width="200" Height="23"/>
|
||||
</Popup>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</UserControl>
|
|
@ -0,0 +1,11 @@
|
|||
using Avalonia.Controls;
|
||||
|
||||
namespace IntegrationTestApp;
|
||||
|
||||
public partial class EmbeddingPage : UserControl
|
||||
{
|
||||
public EmbeddingPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
|
@ -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<App>()
|
||||
.UsePlatformDetect()
|
||||
.AfterSetup(builder =>
|
||||
{
|
||||
NativeTextBox.Factory =
|
||||
OperatingSystem.IsWindows() ? new Win32TextBoxFactory() :
|
||||
OperatingSystem.IsMacOS() ? new MacOSTextBoxFactory() :
|
||||
null;
|
||||
})
|
||||
.LogToTrace();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="ControlCatalog.app"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- A list of the Windows versions that this application has been tested on
|
||||
and is designed to work with. Uncomment the appropriate elements
|
||||
and Windows will automatically select the most compatible environment. -->
|
||||
|
||||
<!-- Windows Vista -->
|
||||
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
|
||||
|
||||
<!-- Windows 7 -->
|
||||
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
|
||||
|
||||
<!-- Windows 8 -->
|
||||
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
|
||||
|
||||
<!-- Windows 8.1 -->
|
||||
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
|
@ -0,0 +1,46 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Automation.Peers;
|
||||
using Avalonia.Platform;
|
||||
|
||||
namespace Avalonia.Controls.Automation.Peers;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the root of a native control automation tree hosted by a <see cref="NativeControlHost"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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<AutomationPeer> 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;
|
||||
}
|
||||
}
|
|
@ -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<AutomationPeer>? GetChildrenCore()
|
||||
{
|
||||
if (Owner is NativeControlHost host && host.NativeControlHandle != null)
|
||||
return [new InteropAutomationPeer(host.NativeControlHandle)];
|
||||
return null;
|
||||
}
|
||||
|
||||
private void OnNativeControlHandleChanged(object? sender, EventArgs e) => InvalidateChildren();
|
||||
}
|
|
@ -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<NativeControlHost>(OnFlowDirectionChanged);
|
||||
}
|
||||
|
||||
internal IPlatformHandle? NativeControlHandle
|
||||
{
|
||||
get => _nativeControlHandle;
|
||||
set
|
||||
{
|
||||
if (_nativeControlHandle != value)
|
||||
{
|
||||
_nativeControlHandle = value;
|
||||
NativeControlHandleChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal event EventHandler? NativeControlHandleChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1180,6 +1180,9 @@ interface IAvnAutomationPeer : IUnknown
|
|||
|
||||
IAvnAutomationPeer* GetRootPeer();
|
||||
|
||||
bool IsInteropPeer();
|
||||
[intptr]void* InteropPeer_GetNativeControlHandle();
|
||||
|
||||
bool IsRootProvider();
|
||||
IAvnWindowBase* RootProvider_GetWindow();
|
||||
IAvnAutomationPeer* RootProvider_GetFocus();
|
||||
|
|
|
@ -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<AAP.IRootProvider>() is object ?
|
||||
new RootAutomationNode(peer) :
|
||||
new AutomationNode(peer);
|
||||
if (peer is InteropAutomationPeer interop)
|
||||
return new InteropAutomationNode(interop);
|
||||
else if (peer.GetProvider<AAP.IRootProvider>() is not null)
|
||||
return new RootAutomationNode(peer);
|
||||
else
|
||||
return new AutomationNode(peer);
|
||||
}
|
||||
|
||||
private static UiaControlTypeId ToUiaControlType(AutomationControlType role)
|
||||
|
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// An automation node which serves as the root of an embedded native control automation tree.
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче