Browse Source

Implement support for Android TalkBack (#17704)

* 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
Isabelle Santin 1 year ago
committed by GitHub
parent
commit
57c85955c2
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 38
      src/Android/Avalonia.Android/Automation/ExpandCollapseNodeInfoProvider.cs
  2. 14
      src/Android/Avalonia.Android/Automation/INodeInfoProvider.cs
  3. 35
      src/Android/Avalonia.Android/Automation/InvokeNodeInfoProvider.cs
  4. 48
      src/Android/Avalonia.Android/Automation/NodeInfoProvider.cs
  5. 31
      src/Android/Avalonia.Android/Automation/RangeValueNodeInfoProvider.cs
  6. 53
      src/Android/Avalonia.Android/Automation/ScrollNodeInfoProvider.cs
  7. 37
      src/Android/Avalonia.Android/Automation/SelectionItemNodeInfoProvider.cs
  8. 39
      src/Android/Avalonia.Android/Automation/ToggleNodeInfoProvider.cs
  9. 59
      src/Android/Avalonia.Android/Automation/ValueNodeInfoProvider.cs
  10. 213
      src/Android/Avalonia.Android/AvaloniaAccessHelper.cs
  11. 20
      src/Android/Avalonia.Android/AvaloniaView.cs
  12. 93
      src/Avalonia.Controls/Automation/Peers/EmbeddableControlRootAutomationPeer.cs
  13. 9
      src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs
  14. 20
      src/Avalonia.Controls/Automation/ValuePatternIdentifiers.cs
  15. 9
      src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs

38
src/Android/Avalonia.Android/Automation/ExpandCollapseNodeInfoProvider.cs

@ -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);
}
}
}

14
src/Android/Avalonia.Android/Automation/INodeInfoProvider.cs

@ -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);
}
}

35
src/Android/Avalonia.Android/Automation/InvokeNodeInfoProvider.cs

@ -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;
}
}
}

48
src/Android/Avalonia.Android/Automation/NodeInfoProvider.cs

@ -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);
}
}

31
src/Android/Avalonia.Android/Automation/RangeValueNodeInfoProvider.cs

@ -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
);
}
}
}

53
src/Android/Avalonia.Android/Automation/ScrollNodeInfoProvider.cs

@ -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;
}
}
}

37
src/Android/Avalonia.Android/Automation/SelectionItemNodeInfoProvider.cs

@ -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;
}
}
}

39
src/Android/Avalonia.Android/Automation/ToggleNodeInfoProvider.cs

@ -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;
}
}
}

59
src/Android/Avalonia.Android/Automation/ValueNodeInfoProvider.cs

@ -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;
}
}
}

213
src/Android/Avalonia.Android/AvaloniaAccessHelper.cs

@ -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();
}
}
}

20
src/Android/Avalonia.Android/AvaloniaView.cs

@ -5,6 +5,8 @@ using Android.Content.Res;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using AndroidX.Core.View;
using AndroidX.CustomView.Widget;
using Avalonia.Android.Platform;
using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls;
@ -19,6 +21,7 @@ namespace Avalonia.Android
{
private EmbeddableControlRoot _root;
private readonly ViewImpl _view;
private readonly ExploreByTouchHelper _accessHelper;
private IDisposable? _timerSubscription;
private bool _surfaceCreated;
@ -26,6 +29,7 @@ namespace Avalonia.Android
public AvaloniaView(Context context) : base(context)
{
_view = new ViewImpl(this);
AddView(_view.View);
_root = new EmbeddableControlRoot(_view);
@ -35,6 +39,9 @@ namespace Avalonia.Android
OnConfigurationChanged();
_view.InternalView.SurfaceWindowCreated += InternalView_SurfaceWindowCreated;
_accessHelper = new AvaloniaAccessHelper(this);
ViewCompat.SetAccessibilityDelegate(this, _accessHelper);
}
private void InternalView_SurfaceWindowCreated(object? sender, EventArgs e)
@ -64,10 +71,21 @@ namespace Avalonia.Android
_root = null!;
}
protected override void OnFocusChanged(bool gainFocus, FocusSearchDirection direction, global::Android.Graphics.Rect? previouslyFocusedRect)
{
base.OnFocusChanged(gainFocus, direction, previouslyFocusedRect);
_accessHelper.OnFocusChanged(gainFocus, (int)direction, previouslyFocusedRect);
}
protected override bool DispatchHoverEvent(MotionEvent? e)
{
return _accessHelper.DispatchHoverEvent(e!) || base.DispatchHoverEvent(e);
}
public override bool DispatchKeyEvent(KeyEvent? e)
{
if (!_view.View.DispatchKeyEvent(e))
return base.DispatchKeyEvent(e);
return _accessHelper.DispatchKeyEvent(e!) || base.DispatchKeyEvent(e);
return true;
}

93
src/Avalonia.Controls/Automation/Peers/EmbeddableControlRootAutomationPeer.cs

@ -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();
}
}
}

9
src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs

@ -8,6 +8,7 @@ namespace Avalonia.Automation.Peers
public TextBoxAutomationPeer(TextBox owner)
: base(owner)
{
Owner.PropertyChanged += OwnerPropertyChanged;
}
public new TextBox Owner => (TextBox)base.Owner;
@ -19,5 +20,13 @@ namespace Avalonia.Automation.Peers
{
return AutomationControlType.Edit;
}
protected virtual void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if(e.Property == TextBox.TextProperty)
{
RaisePropertyChangedEvent(ValuePatternIdentifiers.ValueProperty, e.OldValue, e.NewValue);
}
}
}
}

20
src/Avalonia.Controls/Automation/ValuePatternIdentifiers.cs

@ -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();
}
}

9
src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs

@ -1,9 +1,11 @@
using System;
using System.ComponentModel;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Automation;
using Avalonia.Controls.Automation.Peers;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Platform;
using Avalonia.Styling;
namespace Avalonia.Controls.Embedding
{
@ -52,6 +54,11 @@ namespace Avalonia.Controls.Embedding
protected override Type StyleKeyOverride => typeof(EmbeddableControlRoot);
protected override AutomationPeer OnCreateAutomationPeer()
{
return new EmbeddableControlRootAutomationPeer(this);
}
public void Dispose()
{
PlatformImpl?.Dispose();

Loading…
Cancel
Save