From d6d583a16ea3ae68cebd02a12160dfdc48189cc2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 26 Mar 2021 18:31:51 +0100 Subject: [PATCH] Initial implementation of OSX automation. --- .../project.pbxproj | 8 + native/Avalonia.Native/src/OSX/AvnString.h | 1 + native/Avalonia.Native/src/OSX/AvnString.mm | 17 ++ native/Avalonia.Native/src/OSX/automation.h | 16 ++ native/Avalonia.Native/src/OSX/automation.mm | 220 ++++++++++++++++++ native/Avalonia.Native/src/OSX/common.h | 4 + native/Avalonia.Native/src/OSX/main.mm | 16 ++ native/Avalonia.Native/src/OSX/window.h | 1 + native/Avalonia.Native/src/OSX/window.mm | 83 ++++++- src/Avalonia.Native/AutomationNode.cs | 36 +++ src/Avalonia.Native/AutomationNodeFactory.cs | 24 ++ src/Avalonia.Native/AvnAutomationPeer.cs | 97 ++++++++ src/Avalonia.Native/AvnString.cs | 48 ++++ src/Avalonia.Native/Helpers.cs | 11 + src/Avalonia.Native/PopupImpl.cs | 4 +- src/Avalonia.Native/WindowImpl.cs | 4 +- src/Avalonia.Native/WindowImplBase.cs | 15 +- src/Avalonia.Native/avn.idl | 88 +++++++ 18 files changed, 684 insertions(+), 9 deletions(-) create mode 100644 native/Avalonia.Native/src/OSX/automation.h create mode 100644 native/Avalonia.Native/src/OSX/automation.mm create mode 100644 src/Avalonia.Native/AutomationNode.cs create mode 100644 src/Avalonia.Native/AutomationNodeFactory.cs create mode 100644 src/Avalonia.Native/AvnAutomationPeer.cs diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index dba3ee6d31..2a3f7d7e7d 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -29,6 +29,8 @@ AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB661C1D2148230F00291242 /* AppKit.framework */; }; AB661C202148286E00291242 /* window.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB661C1F2148286E00291242 /* window.mm */; }; AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */; }; + BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; }; + BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -61,6 +63,8 @@ AB661C212148288600291242 /* common.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = common.h; sourceTree = ""; }; AB7A61EF2147C815003C5833 /* libAvalonia.Native.OSX.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libAvalonia.Native.OSX.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = platformthreading.mm; sourceTree = ""; }; + BC11A5BC2608D58F0017BAD0 /* automation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = automation.h; sourceTree = ""; }; + BC11A5BD2608D58F0017BAD0 /* automation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = automation.mm; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -94,6 +98,8 @@ AB7A61E62147C814003C5833 = { isa = PBXGroup; children = ( + BC11A5BC2608D58F0017BAD0 /* automation.h */, + BC11A5BD2608D58F0017BAD0 /* automation.mm */, 1A1852DB23E05814008F0DED /* deadlock.mm */, 1A002B9D232135EE00021753 /* app.mm */, 37DDA9B121933371002E132B /* AvnString.h */, @@ -138,6 +144,7 @@ buildActionMask = 2147483647; files = ( 37155CE4233C00EB0034DCE9 /* menu.h in Headers */, + BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -207,6 +214,7 @@ AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */, 1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */, 1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */, + BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */, 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */, 520624B322973F4100C4DCEF /* menu.mm in Sources */, 37A517B32159597E00FBA241 /* Screens.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/AvnString.h b/native/Avalonia.Native/src/OSX/AvnString.h index 3ce83d370a..3b750b11db 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.h +++ b/native/Avalonia.Native/src/OSX/AvnString.h @@ -14,4 +14,5 @@ extern IAvnStringArray* CreateAvnStringArray(NSArray* array); extern IAvnStringArray* CreateAvnStringArray(NSArray* array); extern IAvnStringArray* CreateAvnStringArray(NSString* string); extern IAvnString* CreateByteArray(void* data, int len); +extern NSString* GetNSStringAndRelease(IAvnString* s); #endif /* AvnString_h */ diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index 001cf151d8..df11ad8715 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -141,3 +141,20 @@ IAvnString* CreateByteArray(void* data, int len) { return new AvnStringImpl(data, len); } + +NSString* GetNSStringAndRelease(IAvnString* s) +{ + if (s != nullptr) + { + char* p; + + if (s->Pointer((void**)&p) == S_OK) + { + return [NSString stringWithUTF8String:p]; + } + + s->Release(); + } + + return nullptr; +} diff --git a/native/Avalonia.Native/src/OSX/automation.h b/native/Avalonia.Native/src/OSX/automation.h new file mode 100644 index 0000000000..65e1153248 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/automation.h @@ -0,0 +1,16 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +class IAvnAutomationPeer; + +@interface AvnAutomationNode : NSAccessibilityElement +- (AvnAutomationNode *)initWithPeer:(IAvnAutomationPeer *)peer; +@end + +struct INSAccessibilityHolder +{ + virtual NSObject* _Nonnull GetNSAccessibility () = 0; +}; + +NS_ASSUME_NONNULL_END diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm new file mode 100644 index 0000000000..064b414094 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -0,0 +1,220 @@ +#import "automation.h" +#include "common.h" +#include "AvnString.h" +#include "window.h" + +class AutomationNode : public ComSingleObject, + public INSAccessibilityHolder +{ +private: + NSAccessibilityElement* _node; +public: + FORWARD_IUNKNOWN() + + AutomationNode(NSAccessibilityElement* node) + { + _node = node; + } + + AutomationNode(IAvnAutomationPeer* peer) + { + _node = [[AvnAutomationNode alloc] initWithPeer: peer]; + } + + virtual NSObject* GetNSAccessibility() override + { + return _node; + } +}; + +@implementation AvnAutomationNode +{ + IAvnAutomationPeer* _peer; + NSMutableArray* _children; +} + +- (AvnAutomationNode *)initWithPeer:(IAvnAutomationPeer *)peer +{ + self = [super init]; + _peer = peer; + return self; +} + +- (BOOL)isAccessibilityElement +{ + return _peer->IsControlElement(); +} + +- (NSAccessibilityRole)accessibilityRole +{ + auto controlType = _peer->GetAutomationControlType(); + + switch (controlType) { + case AutomationButton: + return NSAccessibilityButtonRole; + case AutomationCheckBox: + return NSAccessibilityCheckBoxRole; + case AutomationComboBox: + return NSAccessibilityPopUpButtonRole; + case AutomationGroup: + case AutomationPane: + return NSAccessibilityGroupRole; + case AutomationSlider: + return NSAccessibilitySliderRole; + case AutomationTab: + return NSAccessibilityTabGroupRole; + case AutomationTabItem: + return NSAccessibilityRadioButtonRole; + case AutomationWindow: + return NSAccessibilityWindowRole; + default: + return NSAccessibilityUnknownRole; + } +} + +- (NSString *)accessibilityIdentifier +{ + return GetNSStringAndRelease(_peer->GetAutomationId()); +} + +- (NSString *)accessibilityTitle +{ + return GetNSStringAndRelease(_peer->GetName()); +} + +- (NSArray *)accessibilityChildren +{ + if (_children == nullptr && _peer != nullptr) + { + auto childPeers = _peer->GetChildren(); + auto childCount = childPeers != nullptr ? childPeers->GetCount() : 0; + + if (childCount > 0) + { + _children = [[NSMutableArray alloc] initWithCapacity:childCount]; + + for (int i = 0; i < childCount; ++i) + { + IAvnAutomationPeer* child; + + if (childPeers->Get(i, &child) == S_OK) + { + NSObject* element = ::GetAccessibilityElement(child->GetNode()); + [_children addObject:element]; + } + } + } + } + + return _children; +} + +- (NSRect)accessibilityFrame +{ + auto view = [self getAvnView]; + auto window = [self getAvnWindow]; + + if (view != nullptr) + { + auto bounds = ToNSRect(_peer->GetBoundingRectangle()); + auto windowBounds = [view convertRect:bounds toView:nil]; + auto screenBounds = [window convertRectToScreen:windowBounds]; + return screenBounds; + } + + return NSRect(); +} + +- (id)accessibilityParent +{ + auto parentPeer = _peer->GetParent(); + + if (parentPeer != nullptr) + { + return GetAccessibilityElement(parentPeer); + } + + return [NSApplication sharedApplication]; +} + +- (id)accessibilityTopLevelUIElement +{ + return GetAccessibilityElement([self getRootNode]); +} + +- (id)accessibilityWindow +{ + return [self accessibilityTopLevelUIElement]; +} + +- (BOOL)accessibilityPerformPress +{ + _peer->InvokeProvider_Invoke(); + return YES; +} + +- (BOOL)isAccessibilitySelectorAllowed:(SEL)selector +{ + if (selector == @selector(accessibilityPerformPress)) + { + return _peer->IsInvokeProvider(); + } + + return [super isAccessibilitySelectorAllowed:selector]; +} + +- (IAvnAutomationNode*)getRootNode +{ + auto rootPeer = _peer->GetRootPeer(); + return rootPeer != nullptr ? rootPeer->GetNode() : nullptr; +} + +- (IAvnWindowBase*)getWindow +{ + auto rootNode = [self getRootNode]; + + if (rootNode != nullptr) + { + IAvnWindowBase* window; + if (rootNode->QueryInterface(&IID_IAvnWindow, (void**)&window) == S_OK) + { + return window; + } + } + + return nullptr; +} + +- (AvnWindow*) getAvnWindow +{ + auto window = [self getWindow]; + return dynamic_cast(window)->GetNSWindow(); +} + +- (AvnView*) getAvnView +{ + auto window = [self getWindow]; + return dynamic_cast(window)->GetNSView(); +} + +@end + +extern IAvnAutomationNode* CreateAutomationNode(IAvnAutomationPeer* peer) +{ + @autoreleasepool + { + return new AutomationNode(peer); + } +} + +extern NSObject* GetAccessibilityElement(IAvnAutomationPeer* peer) +{ + auto node = peer != nullptr ? peer->GetNode() : nullptr; + return GetAccessibilityElement(node); +} + +extern NSObject* GetAccessibilityElement(IAvnAutomationNode* node) +{ + auto holder = dynamic_cast(node); + return holder != nullptr ? holder->GetNSAccessibility() : nil; +} diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index c082003ccf..7c00ebc469 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -30,10 +30,14 @@ extern IAvnMenu* GetAppMenu (); extern NSMenuItem* GetAppMenuItem (); extern void SetAutoGenerateDefaultAppMenuItems (bool enabled); extern bool GetAutoGenerateDefaultAppMenuItems (); +extern IAvnAutomationNode* CreateAutomationNode(IAvnAutomationPeer* peer); +extern NSObject* GetAccessibilityElement(IAvnAutomationPeer* peer); +extern NSObject* GetAccessibilityElement(IAvnAutomationNode* node); extern void InitializeAvnApp(IAvnApplicationEvents* events); extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; extern NSPoint ToNSPoint (AvnPoint p); +extern NSRect ToNSRect (AvnRect r); extern AvnPoint ToAvnPoint (NSPoint p); extern AvnPoint ConvertPointY (AvnPoint p); extern CGFloat PrimaryDisplayHeight(); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index aaaf381b26..f231ff9a71 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -1,6 +1,7 @@ //This file will contain actual IID structures #define COM_GUIDS_MATERIALIZE #include "common.h" +#include "window.h" static bool s_generateDefaultAppMenuItems = true; static NSString* s_appTitle = @"Avalonia"; @@ -259,6 +260,12 @@ public: return S_OK; } + virtual HRESULT CreateAutomationNode (IAvnAutomationPeer* peer, IAvnAutomationNode** ppv) override + { + *ppv = ::CreateAutomationNode(peer); + return S_OK; + } + virtual HRESULT SetAppMenu (IAvnMenu* appMenu) override { ::SetAppMenu(s_appTitle, appMenu); @@ -295,6 +302,15 @@ NSPoint ToNSPoint (AvnPoint p) return result; } +NSRect ToNSRect (AvnRect r) +{ + return NSRect + { + NSPoint { r.X, r.Y }, + NSSize { r.Width, r.Height } + }; +} + AvnPoint ToAvnPoint (NSPoint p) { AvnPoint result; diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index b1f64bca88..200b442fcd 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -39,6 +39,7 @@ class WindowBaseImpl; struct INSWindowHolder { virtual AvnWindow* _Nonnull GetNSWindow () = 0; + virtual AvnView* _Nonnull GetNSView () = 0; }; struct IWindowStateChanged diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 870345e543..dc009dc5a0 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -5,14 +5,25 @@ #include "menu.h" #include #include "rendertarget.h" +#include "AvnString.h" +#include "automation.h" -class WindowBaseImpl : public virtual ComSingleObject, public INSWindowHolder +class WindowBaseImpl : public virtual ComObject, + public virtual IAvnWindowBase, + public virtual IAvnAutomationNode, + public INSWindowHolder, + public INSAccessibilityHolder { private: NSCursor* cursor; public: FORWARD_IUNKNOWN() + BEGIN_INTERFACE_MAP() + INTERFACE_MAP_ENTRY(IAvnWindowBase, IID_IAvnWindowBase) + INTERFACE_MAP_ENTRY(IAvnAutomationNode, IID_IAvnAutomationNode) + END_INTERFACE_MAP() + virtual ~WindowBaseImpl() { View = NULL; @@ -104,6 +115,16 @@ public: { return Window; } + + virtual AvnView* GetNSView() override + { + return View; + } + + virtual NSObject* GetNSAccessibility() override + { + return Window; + } virtual HRESULT Show(bool activate) override { @@ -1846,6 +1867,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent bool _isExtended; AvnMenu* _menu; double _lastScaling; + IAvnAutomationPeer* _automationPeer; + NSMutableArray* _automationChildren; } -(void) setIsExtended:(bool)value; @@ -2218,6 +2241,64 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _parent->BaseEvents->PositionChanged(position); } } + +- (BOOL)isAccessibilityElement +{ + [self getAutomationPeer]; + return YES; +} + +- (NSString *)accessibilityIdentifier +{ + auto peer = [self getAutomationPeer]; + return GetNSStringAndRelease(peer->GetAutomationId()); +} + +- (NSArray *)accessibilityChildren +{ + auto peer = [self getAutomationPeer]; + + if (_automationChildren == nullptr) + { + _automationChildren = (NSMutableArray*)[super accessibilityChildren]; + + auto childPeers = peer->GetChildren(); + auto childCount = childPeers != nullptr ? childPeers->GetCount() : 0; + + if (childCount > 0) + { + for (int i = 0; i < childCount; ++i) + { + IAvnAutomationPeer* child; + + if (childPeers->Get(i, &child) == S_OK) + { + auto element = GetAccessibilityElement(child); + [_automationChildren addObject:element]; + } + } + } + } + + return _automationChildren; +} + +- (id)accessibilityHitTest:(NSPoint)point +{ + point = [self convertPointFromScreen:point]; + auto p = [_parent->View translateLocalPoint:ToAvnPoint(point)]; + auto peer = [self getAutomationPeer]; + auto hit = peer->RootProvider_GetPeerFromPoint(p); + return GetAccessibilityElement(hit); +} + +- (IAvnAutomationPeer* _Nonnull) getAutomationPeer +{ + if (_automationPeer == nullptr) + _automationPeer = _parent->BaseEvents->AutomationStarted(_parent); + return _automationPeer; +} + @end class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup diff --git a/src/Avalonia.Native/AutomationNode.cs b/src/Avalonia.Native/AutomationNode.cs new file mode 100644 index 0000000000..c45ade520a --- /dev/null +++ b/src/Avalonia.Native/AutomationNode.cs @@ -0,0 +1,36 @@ +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; +using Avalonia.Native.Interop; + +#nullable enable + +namespace Avalonia.Native +{ + internal class AutomationNode : IRootAutomationNode + { + public AutomationNode(AutomationNodeFactory factory, IAvnAutomationNode native) + { + Native = native; + Factory = factory; + } + + public IAvnAutomationNode Native { get; } + public IAutomationNodeFactory Factory { get; } + + public void ChildrenChanged() + { + // TODO + } + + public void PropertyChanged(AutomationProperty property, object? oldValue, object? newValue) + { + // TODO + } + + public void FocusChanged(AutomationPeer? focus) + { + // TODO + } + } +} diff --git a/src/Avalonia.Native/AutomationNodeFactory.cs b/src/Avalonia.Native/AutomationNodeFactory.cs new file mode 100644 index 0000000000..d40009a855 --- /dev/null +++ b/src/Avalonia.Native/AutomationNodeFactory.cs @@ -0,0 +1,24 @@ +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; +using Avalonia.Native.Interop; + +namespace Avalonia.Native +{ + internal class AutomationNodeFactory : IAutomationNodeFactory + { + private static AutomationNodeFactory _instance; + private readonly IAvaloniaNativeFactory _native; + + public static AutomationNodeFactory GetInstance(IAvaloniaNativeFactory native) + { + return _instance ??= new AutomationNodeFactory(native); + } + + private AutomationNodeFactory(IAvaloniaNativeFactory native) => _native = native; + + public IAutomationNode CreateNode(AutomationPeer peer) + { + return new AutomationNode(this, _native.CreateAutomationNode(new AvnAutomationPeer(peer))); + } + } +} diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs new file mode 100644 index 0000000000..a1519d8d26 --- /dev/null +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; +using Avalonia.Native.Interop; + +#nullable enable + +namespace Avalonia.Native +{ + internal class AvnAutomationPeer : CallbackBase, IAvnAutomationPeer + { + private readonly AutomationPeer _inner; + + public AvnAutomationPeer(AutomationPeer inner) => _inner = inner; + + public IAvnAutomationNode Node => ((AutomationNode)_inner.Node).Native; + public IAvnString? AcceleratorKey => _inner.GetAcceleratorKey().ToAvnString(); + public IAvnString? AccessKey => _inner.GetAccessKey().ToAvnString(); + public AvnAutomationControlType AutomationControlType => (AvnAutomationControlType)_inner.GetAutomationControlType(); + public IAvnString? AutomationId => _inner.GetAutomationId().ToAvnString(); + public AvnRect BoundingRectangle => _inner.GetBoundingRectangle().ToAvnRect(); + public IAvnAutomationPeerArray Children => new AvnAutomationPeerArray(_inner.GetChildren()); + public IAvnString ClassName => _inner.GetClassName().ToAvnString(); + public IAvnAutomationPeer? LabeledBy => Wrap(_inner.GetLabeledBy()); + public IAvnString Name => _inner.GetName().ToAvnString(); + public IAvnAutomationPeer? Parent => Wrap(_inner.GetParent()); + + public int HasKeyboardFocus() => _inner.HasKeyboardFocus().AsComBool(); + public int IsContentElement() => _inner.IsContentElement().AsComBool(); + public int IsControlElement() => _inner.IsControlElement().AsComBool(); + public int IsEnabled() => _inner.IsEnabled().AsComBool(); + public int IsKeyboardFocusable() => _inner.IsKeyboardFocusable().AsComBool(); + public void SetFocus() => _inner.SetFocus(); + public int ShowContextMenu() => _inner.ShowContextMenu().AsComBool(); + + public IAvnAutomationPeer? RootPeer + { + get + { + var peer = _inner; + var parent = peer.GetParent(); + + while (!(peer is IRootProvider) && parent is object) + { + peer = parent; + parent = peer.GetParent(); + } + + return new AvnAutomationPeer(peer); + } + } + + public int IsRootProvider() => (_inner is IRootProvider).AsComBool(); + + public IAvnAutomationPeer? RootProvider_GetPeerFromPoint(AvnPoint point) + { + var result = ((IRootProvider)_inner).GetPeerFromPoint(point.ToAvaloniaPoint()); + + if (result is null) + return null; + + // The OSX accessibility APIs expect non-ignored elements when hit-testing. + while (!result.IsControlElement()) + { + var parent = result.GetParent(); + + if (parent is object) + result = parent; + else + break; + } + + return Wrap(result); + } + + public int IsInvokeProvider() => (_inner is IInvokeProvider).AsComBool(); + + public void InvokeProvider_Invoke() => ((IInvokeProvider)_inner).Invoke(); + + public static AvnAutomationPeer? Wrap(AutomationPeer? peer) => + peer != null ? new AvnAutomationPeer(peer) : null; + } + + internal class AvnAutomationPeerArray : CallbackBase, IAvnAutomationPeerArray + { + private readonly AvnAutomationPeer[] _items; + + public AvnAutomationPeerArray(IReadOnlyList items) + { + _items = items.Select(x => new AvnAutomationPeer(x)).ToArray(); + } + + public uint Count => (uint)_items.Length; + public IAvnAutomationPeer Get(uint index) => _items[index]; + } +} diff --git a/src/Avalonia.Native/AvnString.cs b/src/Avalonia.Native/AvnString.cs index dcd473bae3..bcaa16c5b2 100644 --- a/src/Avalonia.Native/AvnString.cs +++ b/src/Avalonia.Native/AvnString.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using System.Text; namespace Avalonia.Native.Interop { @@ -13,6 +14,53 @@ namespace Avalonia.Native.Interop { string[] ToStringArray(); } + + internal class AvnString : CallbackBase, IAvnString + { + private IntPtr _native; + private int _nativeLen; + + public AvnString(string s) => String = s; + + public string String { get; } + public byte[] Bytes => Encoding.UTF8.GetBytes(String); + + public unsafe void* Pointer() + { + EnsureNative(); + return _native.ToPointer(); + } + + public int Length() + { + EnsureNative(); + return _nativeLen; + } + + protected override void Destroyed() + { + if (_native != IntPtr.Zero) + { + Marshal.FreeHGlobal(_native); + _native = IntPtr.Zero; + } + } + + private unsafe void EnsureNative() + { + if (string.IsNullOrEmpty(String)) + return; + if (_native == IntPtr.Zero) + { + _nativeLen = Encoding.UTF8.GetByteCount(String); + _native = Marshal.AllocHGlobal(_nativeLen + 1); + var ptr = (byte*)_native.ToPointer(); + fixed (char* chars = String) + Encoding.UTF8.GetBytes(chars, String.Length, ptr, _nativeLen); + ptr[_nativeLen] = 0; + } + } + } } namespace Avalonia.Native.Interop.Impl { diff --git a/src/Avalonia.Native/Helpers.cs b/src/Avalonia.Native/Helpers.cs index 564434a04c..764ff789dc 100644 --- a/src/Avalonia.Native/Helpers.cs +++ b/src/Avalonia.Native/Helpers.cs @@ -1,4 +1,5 @@ using Avalonia.Native.Interop; +using JetBrains.Annotations; namespace Avalonia.Native { @@ -24,11 +25,21 @@ namespace Avalonia.Native return new AvnPoint { X = pt.X, Y = pt.Y }; } + public static AvnRect ToAvnRect (this Rect rect) + { + return new AvnRect() { X = rect.X, Y= rect.Y, Height = rect.Height, Width = rect.Width }; + } + public static AvnSize ToAvnSize (this Size size) { return new AvnSize { Height = size.Height, Width = size.Width }; } + public static IAvnString ToAvnString(this string s) + { + return s != null ? new AvnString(s) : null; + } + public static Size ToAvaloniaSize (this AvnSize size) { return new Size(size.Width, size.Height); diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index c36675afcd..83f2cc2f82 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -7,7 +7,6 @@ namespace Avalonia.Native { class PopupImpl : WindowBaseImpl, IPopupImpl { - private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; private readonly AvaloniaNativePlatformOpenGlInterface _glFeature; private readonly IWindowBaseImpl _parent; @@ -15,9 +14,8 @@ namespace Avalonia.Native public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, AvaloniaNativePlatformOpenGlInterface glFeature, - IWindowBaseImpl parent) : base(opts, glFeature) + IWindowBaseImpl parent) : base(factory, opts, glFeature) { - _factory = factory; _opts = opts; _glFeature = glFeature; _parent = parent; diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index f3b60f07be..05cfb5b2ed 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -13,7 +13,6 @@ namespace Avalonia.Native { internal class WindowImpl : WindowBaseImpl, IWindowImpl, ITopLevelImplWithNativeMenuExporter { - private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; private readonly AvaloniaNativePlatformOpenGlInterface _glFeature; IAvnWindow _native; @@ -22,9 +21,8 @@ namespace Avalonia.Native internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - AvaloniaNativePlatformOpenGlInterface glFeature) : base(opts, glFeature) + AvaloniaNativePlatformOpenGlInterface glFeature) : base(factory, opts, glFeature) { - _factory = factory; _opts = opts; _glFeature = glFeature; _doubleClickHelper = new DoubleClickHelper(); diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index b45a0787b9..1e0d7ca0f4 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -48,6 +48,7 @@ namespace Avalonia.Native internal abstract class WindowBaseImpl : IWindowBaseImpl, IFramebufferPlatformSurface, ITopLevelImplWithNativeControlHost { + protected readonly IAvaloniaNativeFactory _factory; protected IInputRoot _inputRoot; IAvnWindowBase _native; private object _syncRoot = new object(); @@ -63,8 +64,10 @@ namespace Avalonia.Native private NativeControlHostImpl _nativeControlHost; private IGlContext _glContext; - internal WindowBaseImpl(AvaloniaNativePlatformOptions opts, AvaloniaNativePlatformOpenGlInterface glFeature) + internal WindowBaseImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, + AvaloniaNativePlatformOpenGlInterface glFeature) { + _factory = factory; _gpu = opts.UseGpu && glFeature != null; _deferredRendering = opts.UseDeferredRendering; @@ -92,6 +95,8 @@ namespace Avalonia.Native Resize(new Size(monitor.WorkingArea.Width * 0.75d, monitor.WorkingArea.Height * 0.7d)); } + public IAvnWindowBase Native => _native; + public Size ClientSize { get @@ -244,8 +249,14 @@ namespace Avalonia.Native return (AvnDragDropEffects)args.Effects; } } - } + public IAvnAutomationPeer AutomationStarted(IAvnAutomationNode node) + { + var factory = AutomationNodeFactory.GetInstance(_parent._factory); + return new AvnAutomationPeer(_parent.AutomationStarted(new AutomationNode(factory, node))); + } + } + public void Activate() { _native.Activate(); diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index adcbeb2d3a..8357742f55 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -400,6 +400,49 @@ enum AvnExtendClientAreaChromeHints AvnDefaultChrome = AvnPreferSystemChrome, } +enum AvnAutomationControlType +{ + AutomationButton, + AutomationCalendar, + AutomationCheckBox, + AutomationComboBox, + AutomationEdit, + AutomationHyperlink, + AutomationImage, + AutomationListItem, + AutomationList, + AutomationMenu, + AutomationMenuBar, + AutomationMenuItem, + AutomationProgressBar, + AutomationRadioButton, + AutomationScrollBar, + AutomationSlider, + AutomationSpinner, + AutomationStatusBar, + AutomationTab, + AutomationTabItem, + AutomationText, + AutomationToolBar, + AutomationToolTip, + AutomationTree, + AutomationTreeItem, + AutomationCustom, + AutomationGroup, + AutomationThumb, + AutomationDataGrid, + AutomationDataItem, + AutomationDocument, + AutomationSplitButton, + AutomationWindow, + AutomationPane, + AutomationHeader, + AutomationHeaderItem, + AutomationTable, + AutomationTitleBar, + AutomationSeparator, +} + [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)] interface IAvaloniaNativeFactory : IUnknown { @@ -418,6 +461,7 @@ interface IAvaloniaNativeFactory : IUnknown HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv); HRESULT CreateMenuItem(IAvnMenuItem** ppv); HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv); + HRESULT CreateAutomationNode(IAvnAutomationPeer* peer, IAvnAutomationNode** ppv); } [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)] @@ -506,6 +550,7 @@ interface IAvnWindowBaseEvents : IUnknown AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, AvnInputModifiers modifiers, AvnDragDropEffects effects, IAvnClipboard* clipboard, [intptr]void* dataObjectHandle); + IAvnAutomationPeer* AutomationStarted(IAvnAutomationNode* node); } [uuid(1ae178ee-1fcc-447f-b6dd-b7bb727f934c)] @@ -733,3 +778,46 @@ interface IAvnApplicationEvents : IUnknown { void FilesOpened (IAvnStringArray* urls); } + +[uuid(b87016f3-7eec-41de-b385-07844c268dc4)] +interface IAvnAutomationPeer : IUnknown +{ + IAvnAutomationNode* GetNode(); + IAvnString* GetAcceleratorKey(); + IAvnString* GetAccessKey(); + AvnAutomationControlType GetAutomationControlType(); + IAvnString* GetAutomationId(); + AvnRect GetBoundingRectangle(); + IAvnAutomationPeerArray* GetChildren(); + IAvnString* GetClassName(); + IAvnAutomationPeer* GetLabeledBy(); + IAvnString* GetName(); + IAvnAutomationPeer* GetParent(); + bool HasKeyboardFocus(); + bool IsContentElement(); + bool IsControlElement(); + bool IsEnabled(); + bool IsKeyboardFocusable(); + void SetFocus(); + bool ShowContextMenu(); + + IAvnAutomationPeer* GetRootPeer(); + + bool IsRootProvider(); + IAvnAutomationPeer* RootProvider_GetPeerFromPoint(AvnPoint point); + + bool IsInvokeProvider(); + void InvokeProvider_Invoke(); +} + +[uuid(b00af5da-78af-4b33-bfff-4ce13a6239a9)] +interface IAvnAutomationPeerArray : IUnknown +{ + uint GetCount(); + HRESULT Get(uint index, IAvnAutomationPeer**ppv); +} + +[uuid(004dc40b-e435-49dc-bac5-6272ee35382a)] +interface IAvnAutomationNode : IUnknown +{ +}