18 changed files with 684 additions and 9 deletions
@ -0,0 +1,16 @@ |
|||
#import <Cocoa/Cocoa.h> |
|||
|
|||
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 |
|||
@ -0,0 +1,220 @@ |
|||
#import "automation.h" |
|||
#include "common.h" |
|||
#include "AvnString.h" |
|||
#include "window.h" |
|||
|
|||
class AutomationNode : public ComSingleObject<IAvnAutomationNode, &IID_IAvnAutomationNode>, |
|||
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<INSWindowHolder*>(window)->GetNSWindow(); |
|||
} |
|||
|
|||
- (AvnView*) getAvnView |
|||
{ |
|||
auto window = [self getWindow]; |
|||
return dynamic_cast<INSWindowHolder*>(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<INSAccessibilityHolder*>(node); |
|||
return holder != nullptr ? holder->GetNSAccessibility() : nil; |
|||
} |
|||
@ -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
|
|||
} |
|||
} |
|||
} |
|||
@ -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))); |
|||
} |
|||
} |
|||
} |
|||
@ -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<AutomationPeer> items) |
|||
{ |
|||
_items = items.Select(x => new AvnAutomationPeer(x)).ToArray(); |
|||
} |
|||
|
|||
public uint Count => (uint)_items.Length; |
|||
public IAvnAutomationPeer Get(uint index) => _items[index]; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue