Browse Source
* Start working on Android explore by touch * Start working on a more serious solution * Reflectionless approach * Allow multiple providers to be defined for the same AutomationPeer instance * Implement EmbeddableControlRootAutomationPeer * Garbage collection * It's working!! * Get better readouts * Implement rest of providers and improve performance * Some cleanup for the PR! * Whoopsie! * Better text readouts for more descriptive elements * Fix bug with previous approach * Some final tweaks * Last tweak! * Slight improvements * Undo last change * Fix bug where custom provider types would not be registered * Better TextBox compatibility with screen readers & TalkBack * Fix regression for LabeledBy tests * Clean up provider code * Final batch of fixes for TextBox behavior * Append text instead of replacing it to fix buggy screen readers * Even more fixes for buggy screen readers * Remove english-specific state descriptions * Code review improvements --------- Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com>pull/18008/head
committed by
GitHub
15 changed files with 716 additions and 2 deletions
@ -0,0 +1,38 @@ |
|||
using Android.OS; |
|||
using AndroidX.Core.View.Accessibility; |
|||
using AndroidX.CustomView.Widget; |
|||
using Avalonia.Automation.Peers; |
|||
using Avalonia.Automation.Provider; |
|||
|
|||
namespace Avalonia.Android.Automation |
|||
{ |
|||
internal class ExpandCollapseNodeInfoProvider : NodeInfoProvider<IExpandCollapseProvider> |
|||
{ |
|||
public ExpandCollapseNodeInfoProvider(ExploreByTouchHelper owner, AutomationPeer peer, int virtualViewId) : |
|||
base(owner, peer, virtualViewId) |
|||
{ |
|||
} |
|||
|
|||
public override bool PerformNodeAction(int action, Bundle? arguments) |
|||
{ |
|||
IExpandCollapseProvider provider = GetProvider(); |
|||
switch (action) |
|||
{ |
|||
case AccessibilityNodeInfoCompat.ActionExpand: |
|||
provider.Expand(); |
|||
return true; |
|||
case AccessibilityNodeInfoCompat.ActionCollapse: |
|||
provider.Collapse(); |
|||
return true; |
|||
default: |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
public override void PopulateNodeInfo(AccessibilityNodeInfoCompat nodeInfo) |
|||
{ |
|||
nodeInfo.AddAction(AccessibilityNodeInfoCompat.ActionExpand); |
|||
nodeInfo.AddAction(AccessibilityNodeInfoCompat.ActionCollapse); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using Android.OS; |
|||
using AndroidX.Core.View.Accessibility; |
|||
|
|||
namespace Avalonia.Android.Automation |
|||
{ |
|||
public interface INodeInfoProvider |
|||
{ |
|||
int VirtualViewId { get; } |
|||
|
|||
bool PerformNodeAction(int action, Bundle? arguments); |
|||
|
|||
void PopulateNodeInfo(AccessibilityNodeInfoCompat nodeInfo); |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
using Android.OS; |
|||
using AndroidX.Core.View.Accessibility; |
|||
using AndroidX.CustomView.Widget; |
|||
using Avalonia.Automation.Peers; |
|||
using Avalonia.Automation.Provider; |
|||
|
|||
namespace Avalonia.Android.Automation |
|||
{ |
|||
internal class InvokeNodeInfoProvider : NodeInfoProvider<IInvokeProvider> |
|||
{ |
|||
public InvokeNodeInfoProvider(ExploreByTouchHelper owner, AutomationPeer peer, int virtualViewId) : |
|||
base(owner, peer, virtualViewId) |
|||
{ |
|||
} |
|||
|
|||
public override bool PerformNodeAction(int action, Bundle? arguments) |
|||
{ |
|||
IInvokeProvider provider = GetProvider(); |
|||
switch (action) |
|||
{ |
|||
case AccessibilityNodeInfoCompat.ActionClick: |
|||
provider.Invoke(); |
|||
return true; |
|||
default: |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
public override void PopulateNodeInfo(AccessibilityNodeInfoCompat nodeInfo) |
|||
{ |
|||
nodeInfo.AddAction(AccessibilityNodeInfoCompat.ActionClick); |
|||
nodeInfo.Clickable = true; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
using System; |
|||
using Android.OS; |
|||
using AndroidX.Core.View.Accessibility; |
|||
using AndroidX.CustomView.Widget; |
|||
using Avalonia.Automation; |
|||
using Avalonia.Automation.Peers; |
|||
|
|||
namespace Avalonia.Android.Automation |
|||
{ |
|||
internal delegate INodeInfoProvider NodeInfoProviderInitializer(ExploreByTouchHelper owner, AutomationPeer peer, int virtualViewId); |
|||
|
|||
internal abstract class NodeInfoProvider<T> : INodeInfoProvider |
|||
{ |
|||
private readonly ExploreByTouchHelper _owner; |
|||
|
|||
private readonly AutomationPeer _peer; |
|||
|
|||
public int VirtualViewId { get; } |
|||
|
|||
public NodeInfoProvider(ExploreByTouchHelper owner, AutomationPeer peer, int virtualViewId) |
|||
{ |
|||
_owner = owner; |
|||
_peer = peer; |
|||
VirtualViewId = virtualViewId; |
|||
|
|||
_peer.PropertyChanged += PeerPropertyChanged; |
|||
} |
|||
|
|||
protected void InvalidateSelf() |
|||
{ |
|||
_owner.InvalidateVirtualView(VirtualViewId); |
|||
} |
|||
|
|||
protected void InvalidateSelf(int changeTypes) |
|||
{ |
|||
_owner.InvalidateVirtualView(VirtualViewId, changeTypes); |
|||
} |
|||
|
|||
protected virtual void PeerPropertyChanged(object? sender, AutomationPropertyChangedEventArgs e) { } |
|||
|
|||
public T GetProvider() => _peer.GetProvider<T>() ?? |
|||
throw new InvalidOperationException($"Peer instance does not implement {nameof(T)}."); |
|||
|
|||
public abstract bool PerformNodeAction(int action, Bundle? arguments); |
|||
|
|||
public abstract void PopulateNodeInfo(AccessibilityNodeInfoCompat nodeInfo); |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
using Android.OS; |
|||
using AndroidX.Core.View.Accessibility; |
|||
using AndroidX.CustomView.Widget; |
|||
using Avalonia.Automation.Peers; |
|||
using Avalonia.Automation.Provider; |
|||
|
|||
namespace Avalonia.Android.Automation |
|||
{ |
|||
internal class RangeValueNodeInfoProvider : NodeInfoProvider<IRangeValueProvider> |
|||
{ |
|||
public RangeValueNodeInfoProvider(ExploreByTouchHelper owner, AutomationPeer peer, int virtualViewId) : |
|||
base(owner, peer, virtualViewId) |
|||
{ |
|||
} |
|||
|
|||
public override bool PerformNodeAction(int action, Bundle? arguments) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
public override void PopulateNodeInfo(AccessibilityNodeInfoCompat nodeInfo) |
|||
{ |
|||
IRangeValueProvider provider = GetProvider(); |
|||
nodeInfo.RangeInfo = new AccessibilityNodeInfoCompat.RangeInfoCompat( |
|||
AccessibilityNodeInfoCompat.RangeInfoCompat.RangeTypeFloat, |
|||
(float)provider.Minimum, (float)provider.Maximum, |
|||
(float)provider.Value |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
using Android.OS; |
|||
using AndroidX.Core.View.Accessibility; |
|||
using AndroidX.CustomView.Widget; |
|||
using Avalonia.Automation.Peers; |
|||
using Avalonia.Automation.Provider; |
|||
|
|||
namespace Avalonia.Android.Automation |
|||
{ |
|||
internal class ScrollNodeInfoProvider : NodeInfoProvider<IScrollProvider> |
|||
{ |
|||
public ScrollNodeInfoProvider(ExploreByTouchHelper owner, AutomationPeer peer, int virtualViewId) : |
|||
base(owner, peer, virtualViewId) |
|||
{ |
|||
} |
|||
|
|||
public override bool PerformNodeAction(int action, Bundle? arguments) |
|||
{ |
|||
IScrollProvider provider = GetProvider(); |
|||
switch (action) |
|||
{ |
|||
case AccessibilityNodeInfoCompat.ActionScrollForward: |
|||
if (provider.VerticallyScrollable) |
|||
{ |
|||
provider.Scroll(ScrollAmount.NoAmount, ScrollAmount.SmallIncrement); |
|||
} |
|||
else if(provider.HorizontallyScrollable) |
|||
{ |
|||
provider.Scroll(ScrollAmount.SmallIncrement, ScrollAmount.NoAmount); |
|||
} |
|||
return true; |
|||
case AccessibilityNodeInfoCompat.ActionScrollBackward: |
|||
if (provider.VerticallyScrollable) |
|||
{ |
|||
provider.Scroll(ScrollAmount.NoAmount, ScrollAmount.SmallDecrement); |
|||
} |
|||
else if (provider.HorizontallyScrollable) |
|||
{ |
|||
provider.Scroll(ScrollAmount.SmallDecrement, ScrollAmount.NoAmount); |
|||
} |
|||
return true; |
|||
default: |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
public override void PopulateNodeInfo(AccessibilityNodeInfoCompat nodeInfo) |
|||
{ |
|||
nodeInfo.AddAction(AccessibilityNodeInfoCompat.ActionScrollForward); |
|||
nodeInfo.AddAction(AccessibilityNodeInfoCompat.ActionScrollBackward); |
|||
nodeInfo.Scrollable = true; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
using Android.OS; |
|||
using AndroidX.Core.View.Accessibility; |
|||
using AndroidX.CustomView.Widget; |
|||
using Avalonia.Automation.Peers; |
|||
using Avalonia.Automation.Provider; |
|||
|
|||
namespace Avalonia.Android.Automation |
|||
{ |
|||
internal class SelectionItemNodeInfoProvider : NodeInfoProvider<ISelectionItemProvider> |
|||
{ |
|||
public SelectionItemNodeInfoProvider(ExploreByTouchHelper owner, AutomationPeer peer, int virtualViewId) : |
|||
base(owner, peer, virtualViewId) |
|||
{ |
|||
} |
|||
|
|||
public override bool PerformNodeAction(int action, Bundle? arguments) |
|||
{ |
|||
ISelectionItemProvider provider = GetProvider(); |
|||
switch (action) |
|||
{ |
|||
case AccessibilityNodeInfoCompat.ActionSelect: |
|||
provider.Select(); |
|||
return true; |
|||
default: |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
public override void PopulateNodeInfo(AccessibilityNodeInfoCompat nodeInfo) |
|||
{ |
|||
nodeInfo.AddAction(AccessibilityNodeInfoCompat.ActionSelect); |
|||
|
|||
ISelectionItemProvider provider = GetProvider(); |
|||
nodeInfo.Selected = provider.IsSelected; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using Android.OS; |
|||
using AndroidX.Core.View.Accessibility; |
|||
using AndroidX.CustomView.Widget; |
|||
using Avalonia.Automation.Peers; |
|||
using Avalonia.Automation.Provider; |
|||
|
|||
namespace Avalonia.Android.Automation |
|||
{ |
|||
internal class ToggleNodeInfoProvider : NodeInfoProvider<IToggleProvider> |
|||
{ |
|||
public ToggleNodeInfoProvider(ExploreByTouchHelper owner, AutomationPeer peer, int virtualViewId) : |
|||
base(owner, peer, virtualViewId) |
|||
{ |
|||
} |
|||
|
|||
public override bool PerformNodeAction(int action, Bundle? arguments) |
|||
{ |
|||
IToggleProvider provider = GetProvider(); |
|||
switch (action) |
|||
{ |
|||
case AccessibilityNodeInfoCompat.ActionClick: |
|||
provider.Toggle(); |
|||
return true; |
|||
default: |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
public override void PopulateNodeInfo(AccessibilityNodeInfoCompat nodeInfo) |
|||
{ |
|||
nodeInfo.AddAction(AccessibilityNodeInfoCompat.ActionClick); |
|||
nodeInfo.Clickable = true; |
|||
|
|||
IToggleProvider provider = GetProvider(); |
|||
nodeInfo.Checked = provider.ToggleState == ToggleState.On; |
|||
nodeInfo.Checkable = true; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
using Android.OS; |
|||
using AndroidX.Core.View; |
|||
using AndroidX.Core.View.Accessibility; |
|||
using AndroidX.CustomView.Widget; |
|||
using Avalonia.Automation; |
|||
using Avalonia.Automation.Peers; |
|||
using Avalonia.Automation.Provider; |
|||
|
|||
namespace Avalonia.Android.Automation |
|||
{ |
|||
internal class ValueNodeInfoProvider : NodeInfoProvider<IValueProvider> |
|||
{ |
|||
public ValueNodeInfoProvider(ExploreByTouchHelper owner, AutomationPeer peer, int virtualViewId) : |
|||
base(owner, peer, virtualViewId) |
|||
{ |
|||
} |
|||
|
|||
protected override void PeerPropertyChanged(object? sender, AutomationPropertyChangedEventArgs e) |
|||
{ |
|||
base.PeerPropertyChanged(sender, e); |
|||
if (e.Property == ValuePatternIdentifiers.ValueProperty) |
|||
{ |
|||
InvalidateSelf(AccessibilityEventCompat.ContentChangeTypeText); |
|||
} |
|||
} |
|||
|
|||
public override bool PerformNodeAction(int action, Bundle? arguments) |
|||
{ |
|||
IValueProvider provider = GetProvider(); |
|||
switch (action) |
|||
{ |
|||
case AccessibilityNodeInfoCompat.ActionSetText: |
|||
string? text = arguments?.GetCharSequence( |
|||
AccessibilityNodeInfoCompat.ActionArgumentSetTextCharsequence |
|||
); |
|||
provider.SetValue(provider.Value + text); |
|||
return true; |
|||
|
|||
default: |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
public override void PopulateNodeInfo(AccessibilityNodeInfoCompat nodeInfo) |
|||
{ |
|||
nodeInfo.AddAction(AccessibilityNodeInfoCompat.ActionSetText); |
|||
|
|||
IValueProvider provider = GetProvider(); |
|||
nodeInfo.Text = provider.Value; |
|||
nodeInfo.Editable = !provider.IsReadOnly; |
|||
|
|||
nodeInfo.SetTextSelection( |
|||
provider.Value?.Length ?? 0, |
|||
provider.Value?.Length ?? 0 |
|||
); |
|||
nodeInfo.LiveRegion = ViewCompat.AccessibilityLiveRegionPolite; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,213 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Android.Content.PM; |
|||
using Android.OS; |
|||
using AndroidX.Core.View.Accessibility; |
|||
using AndroidX.CustomView.Widget; |
|||
using Avalonia.Android.Automation; |
|||
using Avalonia.Automation; |
|||
using Avalonia.Automation.Peers; |
|||
using Avalonia.Automation.Provider; |
|||
using Java.Lang; |
|||
|
|||
namespace Avalonia.Android |
|||
{ |
|||
internal class AvaloniaAccessHelper : ExploreByTouchHelper |
|||
{ |
|||
private const string AUTOMATION_PROVIDER_NAMESPACE = "Avalonia.Automation.Provider"; |
|||
|
|||
private static readonly IReadOnlyDictionary<string, NodeInfoProviderInitializer> |
|||
s_providerTypeInitializers = new Dictionary<string, NodeInfoProviderInitializer>() |
|||
{ |
|||
{ typeof(IExpandCollapseProvider).FullName!, (owner, peer, id) => new ExpandCollapseNodeInfoProvider(owner, peer, id) }, |
|||
{ typeof(IInvokeProvider).FullName!, (owner, peer, id) => new InvokeNodeInfoProvider(owner, peer, id) }, |
|||
{ typeof(IRangeValueProvider).FullName!, (owner, peer, id) => new RangeValueNodeInfoProvider(owner, peer, id) }, |
|||
{ typeof(IScrollProvider).FullName!, (owner, peer, id) => new ScrollNodeInfoProvider(owner, peer, id) }, |
|||
{ typeof(ISelectionItemProvider).FullName!, (owner, peer, id) => new SelectionItemNodeInfoProvider(owner, peer, id) }, |
|||
{ typeof(IToggleProvider).FullName!, (owner, peer, id) => new ToggleNodeInfoProvider(owner, peer, id) }, |
|||
{ typeof(IValueProvider).FullName!, (owner, peer, id) => new ValueNodeInfoProvider(owner, peer, id) }, |
|||
}; |
|||
|
|||
private readonly Dictionary<int, AutomationPeer> _peers; |
|||
private readonly Dictionary<AutomationPeer, int> _peerIds; |
|||
|
|||
private readonly Dictionary<AutomationPeer, HashSet<INodeInfoProvider>> _peerNodeInfoProviders; |
|||
|
|||
private readonly AvaloniaView _view; |
|||
|
|||
public AvaloniaAccessHelper(AvaloniaView view) : base(view) |
|||
{ |
|||
_peers = []; |
|||
_peerIds = []; |
|||
_peerNodeInfoProviders = []; |
|||
|
|||
AutomationPeer rootPeer = ControlAutomationPeer.CreatePeerForElement(view.TopLevel!); |
|||
GetOrCreateNodeInfoProvidersFromPeer(rootPeer, out int _); |
|||
|
|||
_view = view; |
|||
} |
|||
|
|||
private HashSet<INodeInfoProvider>? GetNodeInfoProvidersFromVirtualViewId(int virtualViewId) |
|||
{ |
|||
if (_peers.TryGetValue(virtualViewId, out AutomationPeer? peer) && |
|||
_peerNodeInfoProviders.TryGetValue(peer, out HashSet<INodeInfoProvider>? nodeInfoProviders)) |
|||
{ |
|||
return nodeInfoProviders; |
|||
} |
|||
else |
|||
{ |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
private HashSet<INodeInfoProvider> GetOrCreateNodeInfoProvidersFromPeer(AutomationPeer peer, out int virtualViewId) |
|||
{ |
|||
int peerViewId; |
|||
if (_peerNodeInfoProviders.TryGetValue(peer, out HashSet<INodeInfoProvider>? nodeInfoProviders)) |
|||
{ |
|||
peerViewId = _peerIds[peer]; |
|||
} |
|||
else |
|||
{ |
|||
peerViewId = _peerNodeInfoProviders.Count; |
|||
_peers.Add(peerViewId, peer); |
|||
_peerIds.Add(peer, peerViewId); |
|||
|
|||
nodeInfoProviders = new(); |
|||
_peerNodeInfoProviders.Add(peer, nodeInfoProviders); |
|||
|
|||
peer.ChildrenChanged += (s, ev) => InvalidateVirtualView(peerViewId, |
|||
AccessibilityEventCompat.ContentChangeTypeSubtree); |
|||
peer.PropertyChanged += (s, ev) => |
|||
{ |
|||
if (ev.Property == AutomationElementIdentifiers.NameProperty) |
|||
{ |
|||
InvalidateVirtualView(peerViewId, AccessibilityEventCompat.ContentChangeTypeText); |
|||
} |
|||
else if (ev.Property == AutomationElementIdentifiers.HelpTextProperty) |
|||
{ |
|||
InvalidateVirtualView(peerViewId, AccessibilityEventCompat.ContentChangeTypeContentDescription); |
|||
} |
|||
else if (ev.Property == AutomationElementIdentifiers.BoundingRectangleProperty || |
|||
ev.Property == AutomationElementIdentifiers.ClassNameProperty) |
|||
{ |
|||
InvalidateVirtualView(peerViewId); |
|||
} |
|||
}; |
|||
|
|||
Type peerType = peer.GetType(); |
|||
IEnumerable<Type> providerTypes = peerType.GetInterfaces() |
|||
.Where(x => x.Namespace!.StartsWith(AUTOMATION_PROVIDER_NAMESPACE)); |
|||
foreach (Type providerType in providerTypes) |
|||
{ |
|||
if (s_providerTypeInitializers.TryGetValue(providerType.FullName!, out NodeInfoProviderInitializer? ctor)) |
|||
{ |
|||
INodeInfoProvider nodeInfoProvider = ctor(this, peer, peerViewId); |
|||
nodeInfoProviders.Add(nodeInfoProvider); |
|||
} |
|||
} |
|||
} |
|||
|
|||
virtualViewId = peerViewId; |
|||
return nodeInfoProviders; |
|||
} |
|||
|
|||
protected override int GetVirtualViewAt(float x, float y) |
|||
{ |
|||
Point p = _view.TopLevelImpl.PointToClient(new PixelPoint((int)x, (int)y)); |
|||
IEmbeddedRootProvider? embeddedRootProvider = _peers[0].GetProvider<IEmbeddedRootProvider>(); |
|||
AutomationPeer? peer = embeddedRootProvider?.GetPeerFromPoint(p); |
|||
if (peer is not null) |
|||
{ |
|||
GetOrCreateNodeInfoProvidersFromPeer(peer, out int virtualViewId); |
|||
return virtualViewId == 0 ? InvalidId : virtualViewId; |
|||
} |
|||
else |
|||
{ |
|||
peer = embeddedRootProvider?.GetFocus(); |
|||
return peer is null ? InvalidId : _peerIds[peer]; |
|||
} |
|||
} |
|||
|
|||
protected override void GetVisibleVirtualViews(IList<Integer>? virtualViewIds) |
|||
{ |
|||
if (virtualViewIds is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
foreach (AutomationPeer peer in _peers[0].GetChildren()) |
|||
{ |
|||
GetOrCreateNodeInfoProvidersFromPeer(peer, out int virtualViewId); |
|||
virtualViewIds.Add(Integer.ValueOf(virtualViewId)); |
|||
} |
|||
} |
|||
|
|||
protected override bool OnPerformActionForVirtualView(int virtualViewId, int action, Bundle? arguments) |
|||
{ |
|||
return (GetNodeInfoProvidersFromVirtualViewId(virtualViewId) ?? []) |
|||
.Select(x => x.PerformNodeAction(action, arguments)) |
|||
.Aggregate(false, (a, b) => a | b); |
|||
} |
|||
|
|||
protected override void OnPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfoCompat nodeInfo) |
|||
{ |
|||
if (!_peers.TryGetValue(virtualViewId, out AutomationPeer? peer)) |
|||
{ |
|||
return; // BAIL!! No work to be done
|
|||
} |
|||
|
|||
// UI logical structure
|
|||
foreach (AutomationPeer child in peer.GetChildren()) |
|||
{ |
|||
GetOrCreateNodeInfoProvidersFromPeer(child, out int childId); |
|||
nodeInfo.AddChild(_view, childId); |
|||
} |
|||
|
|||
// UI labels
|
|||
AutomationPeer? labeledBy = peer.GetLabeledBy(); |
|||
if (labeledBy is not null) |
|||
{ |
|||
GetOrCreateNodeInfoProvidersFromPeer(labeledBy, out int labeledById); |
|||
nodeInfo.SetLabeledBy(_view, labeledById); |
|||
} |
|||
|
|||
// UI debug metadata
|
|||
nodeInfo.ClassName = peer.GetClassName(); |
|||
nodeInfo.UniqueId = peer.GetAutomationId(); |
|||
|
|||
// Common control state
|
|||
nodeInfo.Enabled = peer.IsEnabled(); |
|||
|
|||
// Control focus state
|
|||
bool canFocusAtAll = peer.IsContentElement() && !peer.IsOffscreen(); |
|||
nodeInfo.ScreenReaderFocusable = canFocusAtAll; |
|||
nodeInfo.Focusable = canFocusAtAll && peer.IsKeyboardFocusable(); |
|||
|
|||
nodeInfo.AccessibilityFocused = peer.HasKeyboardFocus(); |
|||
nodeInfo.Focused = peer.HasKeyboardFocus(); |
|||
|
|||
// On-screen bounds
|
|||
Rect bounds = peer.GetBoundingRectangle(); |
|||
PixelRect screenRect = new PixelRect( |
|||
_view.TopLevelImpl.PointToScreen(bounds.TopLeft), |
|||
_view.TopLevelImpl.PointToScreen(bounds.BottomRight) |
|||
); |
|||
nodeInfo.SetBoundsInParent(new( |
|||
screenRect.X, screenRect.Y, |
|||
screenRect.Right, screenRect.Bottom |
|||
)); |
|||
|
|||
// UI provider specifics
|
|||
foreach (INodeInfoProvider nodeInfoProvider in _peerNodeInfoProviders[peer]) |
|||
{ |
|||
nodeInfoProvider.PopulateNodeInfo(nodeInfo); |
|||
} |
|||
|
|||
// Control text contents
|
|||
nodeInfo.Text ??= peer.GetName(); |
|||
nodeInfo.ContentDescription ??= peer.GetHelpText(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,93 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel; |
|||
using Avalonia.Automation.Peers; |
|||
using Avalonia.Automation.Provider; |
|||
using Avalonia.Controls.Embedding; |
|||
using Avalonia.Input; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Controls.Automation.Peers |
|||
{ |
|||
public class EmbeddableControlRootAutomationPeer : ContentControlAutomationPeer, IEmbeddedRootProvider |
|||
{ |
|||
private Control? _focus; |
|||
|
|||
public EmbeddableControlRootAutomationPeer(EmbeddableControlRoot owner) : base(owner) |
|||
{ |
|||
if (owner.IsVisible) |
|||
StartTrackingFocus(); |
|||
else |
|||
owner.Opened += OnOpened; |
|||
owner.Closed += OnClosed; |
|||
} |
|||
|
|||
public new EmbeddableControlRoot Owner => (EmbeddableControlRoot)base.Owner; |
|||
|
|||
public event EventHandler? FocusChanged; |
|||
|
|||
public AutomationPeer? GetFocus() => _focus is object ? GetOrCreate(_focus) : null; |
|||
|
|||
public AutomationPeer? GetPeerFromPoint(Point p) |
|||
{ |
|||
var hit = Owner.GetVisualAt(p)?.FindAncestorOfType<Control>(); |
|||
|
|||
if (hit is null) |
|||
return null; |
|||
|
|||
var peer = GetOrCreate(hit); |
|||
return peer; |
|||
} |
|||
|
|||
private void StartTrackingFocus() |
|||
{ |
|||
if (KeyboardDevice.Instance is not null) |
|||
{ |
|||
KeyboardDevice.Instance.PropertyChanged += KeyboardDevicePropertyChanged; |
|||
OnFocusChanged(KeyboardDevice.Instance.FocusedElement); |
|||
} |
|||
} |
|||
|
|||
private void StopTrackingFocus() |
|||
{ |
|||
if (KeyboardDevice.Instance is not null) |
|||
KeyboardDevice.Instance.PropertyChanged -= KeyboardDevicePropertyChanged; |
|||
} |
|||
|
|||
private void OnFocusChanged(IInputElement? focus) |
|||
{ |
|||
var oldFocus = _focus; |
|||
var c = focus as Control; |
|||
|
|||
_focus = c?.VisualRoot == Owner ? c : null; |
|||
|
|||
if (_focus != oldFocus) |
|||
{ |
|||
var peer = _focus is object ? |
|||
_focus == Owner ? this : |
|||
GetOrCreate(_focus) : null; |
|||
FocusChanged?.Invoke(this, EventArgs.Empty); |
|||
} |
|||
} |
|||
|
|||
private void KeyboardDevicePropertyChanged(object? sender, PropertyChangedEventArgs e) |
|||
{ |
|||
if (e.PropertyName == nameof(KeyboardDevice.FocusedElement)) |
|||
{ |
|||
OnFocusChanged(KeyboardDevice.Instance!.FocusedElement); |
|||
} |
|||
} |
|||
|
|||
private void OnOpened(object? sender, EventArgs e) |
|||
{ |
|||
Owner.Opened -= OnOpened; |
|||
StartTrackingFocus(); |
|||
} |
|||
|
|||
private void OnClosed(object? sender, EventArgs e) |
|||
{ |
|||
Owner.Closed -= OnClosed; |
|||
StopTrackingFocus(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
using Avalonia.Automation.Provider; |
|||
|
|||
namespace Avalonia.Automation |
|||
{ |
|||
/// <summary>
|
|||
/// Contains values used as identifiers by <see cref="IValueProvider"/>.
|
|||
/// </summary>
|
|||
public static class ValuePatternIdentifiers |
|||
{ |
|||
/// <summary>
|
|||
/// Identifies <see cref="IValueProvider.IsReadOnly"/> automation property.
|
|||
/// </summary>
|
|||
public static AutomationProperty IsReadOnlyProperty { get; } = new AutomationProperty(); |
|||
|
|||
/// <summary>
|
|||
/// Identifies <see cref="IValueProvider.Value"/> automation property.
|
|||
/// </summary>
|
|||
public static AutomationProperty ValueProperty { get; } = new AutomationProperty(); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue