Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic integration tests for NativeControlHost and improve its automation/a11y support. #15542

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 7 additions & 2 deletions native/Avalonia.Native/src/OSX/automation.mm
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ @implementation AvnAccessibilityElement
NSMutableArray* _children;
}

+ (AvnAccessibilityElement *)acquire:(IAvnAutomationPeer *)peer
+ (NSAccessibilityElement *)acquire:(IAvnAutomationPeer *)peer
{
if (peer == nullptr)
return nil;
Expand All @@ -70,7 +70,12 @@ + (AvnAccessibilityElement *)acquire:(IAvnAutomationPeer *)peer
if (instance != nullptr)
return dynamic_cast<AutomationNode*>(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();

Expand Down
9 changes: 9 additions & 0 deletions samples/IntegrationTestApp/Embedding/INativeControlFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;
using Avalonia.Platform;

namespace IntegrationTestApp.Embedding;

internal interface INativeControlFactory
{
IPlatformHandle CreateControl(IPlatformHandle parent, Func<IPlatformHandle> createDefault);
}
16 changes: 16 additions & 0 deletions samples/IntegrationTestApp/Embedding/MacHelper.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
20 changes: 20 additions & 0 deletions samples/IntegrationTestApp/Embedding/MacOSTextBoxFactory.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
19 changes: 19 additions & 0 deletions samples/IntegrationTestApp/Embedding/MacOSViewHandle.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
20 changes: 20 additions & 0 deletions samples/IntegrationTestApp/Embedding/NativeTextBox.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
21 changes: 21 additions & 0 deletions samples/IntegrationTestApp/Embedding/Win32TextBoxFactory.cs
Original file line number Diff line number Diff line change
@@ -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");
}
}
11 changes: 11 additions & 0 deletions samples/IntegrationTestApp/Embedding/Win32WindowControlHandle.cs
Original file line number Diff line number Diff line change
@@ -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);
}
82 changes: 82 additions & 0 deletions samples/IntegrationTestApp/Embedding/WinApi.cs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/microsoft/CsWin32

with NativeMethods.txt file:

CreateWindowEx
GetModuleHandle
DestroyWindow

Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 1 deletion samples/IntegrationTestApp/IntegrationTestApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn);AVP1012</NoWarn>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>

<PropertyGroup>
Expand All @@ -12,13 +13,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>
Expand Down
15 changes: 15 additions & 0 deletions samples/IntegrationTestApp/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:integrationTestApp="using:IntegrationTestApp"
xmlns:embedding="using:IntegrationTestApp.Embedding"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="IntegrationTestApp.MainWindow"
Name="MainWindow"
Expand Down Expand Up @@ -94,6 +95,20 @@
</StackPanel>
</TabItem>

<TabItem Header="Embedding">
<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>
</TabItem>

<TabItem Header="Gestures">
<DockPanel>
<DockPanel DockPanel.Dock="Top">
Expand Down
8 changes: 8 additions & 0 deletions samples/IntegrationTestApp/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using Avalonia;
using IntegrationTestApp.Embedding;

namespace IntegrationTestApp
{
Expand Down Expand Up @@ -31,6 +32,13 @@ public static void Main(string[] args)
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.AfterSetup(builder =>
{
NativeTextBox.Factory =
OperatingSystem.IsWindows() ? new Win32TextBoxFactory() :
OperatingSystem.IsMacOS() ? new MacOSTextBoxFactory() :
null;
})
.LogToTrace();
}
}
28 changes: 28 additions & 0 deletions samples/IntegrationTestApp/app.manifest
Original file line number Diff line number Diff line change
@@ -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>
46 changes: 46 additions & 0 deletions src/Avalonia.Controls/Automation/Peers/InteropAutomationPeer.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}