A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

369 lines
13 KiB

using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Automation.Provider;
using CoreGraphics;
using Foundation;
using UIKit;
namespace Avalonia.iOS
{
public class AutomationPeerWrapper : UIAccessibilityElement, IUIAccessibilityContainer
{
private static readonly IReadOnlyDictionary<AutomationProperty, Action<AutomationPeerWrapper>> s_propertySetters =
new Dictionary<AutomationProperty, Action<AutomationPeerWrapper>>()
{
{ AutomationElementIdentifiers.NameProperty, UpdateName },
{ AutomationElementIdentifiers.HelpTextProperty, UpdateHelpText },
{ AutomationElementIdentifiers.BoundingRectangleProperty, UpdateBoundingRectangle },
{ RangeValuePatternIdentifiers.IsReadOnlyProperty, UpdateIsReadOnly },
{ RangeValuePatternIdentifiers.ValueProperty, UpdateValue },
{ ValuePatternIdentifiers.IsReadOnlyProperty, UpdateIsReadOnly },
{ ValuePatternIdentifiers.ValueProperty, UpdateValue },
};
private readonly AvaloniaView _view;
private readonly AutomationPeer _peer;
private readonly List<AutomationPeer?> _childrenList;
private readonly Dictionary<AutomationPeer, AutomationPeerWrapper> _childrenMap;
[Export("accessibilityContainerType")]
public UIAccessibilityContainerType AccessibilityContainerType { get; set; }
private AutomationPeerWrapper(AutomationPeerWrapper parent, AvaloniaView view, AutomationPeer peer) : base(parent)
{
_view = view;
_peer = peer;
_peer.ChildrenChanged += PeerChildrenChanged;
_peer.PropertyChanged += PeerPropertyChanged;
_childrenList = new();
_childrenMap = new();
}
public AutomationPeerWrapper(AvaloniaView view, AutomationPeer peer) : base(view)
{
_view = view;
_peer = peer;
_peer.ChildrenChanged += PeerChildrenChanged;
_peer.PropertyChanged += PeerPropertyChanged;
_childrenList = new();
_childrenMap = new();
}
[Export("accessibilityElementCount")]
public nint AccessibilityElementCount()
{
UpdateChildren();
return _childrenList.Count;
}
[Export("accessibilityElementAtIndex:")]
public NSObject GetAccessibilityElementAt(nint index)
{
AutomationPeer? child = _childrenList[(int)index];
if (child is not null)
{
return _childrenMap[child];
}
else
{
throw new ArgumentNullException();
}
}
[Export("indexOfAccessibilityElement:")]
public nint GetIndexOfAccessibilityElement(NSObject element)
{
int indexOf = _childrenList.IndexOf((element as AutomationPeerWrapper)?._peer);
return indexOf < 0 ? NSRange.NotFound : indexOf;
}
void UpdateChildren()
{
UpdateAllProperties();
UpdateTraits();
foreach (AutomationPeer child in _peer.GetChildren())
{
AutomationPeerWrapper? wrapper;
if (!_childrenMap.TryGetValue(child, out wrapper) && !child.IsOffscreen())
{
wrapper = new(this, _view, child);
_childrenList.Add(child);
_childrenMap.Add(child, wrapper);
}
else if (child.IsOffscreen())
{
_childrenList.Remove(child);
_childrenMap.Remove(child);
}
wrapper?.UpdateAllProperties();
wrapper?.UpdateTraits();
}
}
private static void UpdateName(AutomationPeerWrapper self)
{
AutomationPeer peer = self;
self.AccessibilityLabel = peer.GetName();
}
private static void UpdateHelpText(AutomationPeerWrapper self)
{
AutomationPeer peer = self;
self.AccessibilityHint = peer.GetHelpText();
}
private static void UpdateBoundingRectangle(AutomationPeerWrapper self)
{
AutomationPeer peer = self;
Rect bounds = peer.GetBoundingRectangle();
PixelRect screenRect = new PixelRect(
self._view.TopLevel.PointToScreen(bounds.TopLeft),
self._view.TopLevel.PointToScreen(bounds.BottomRight)
);
CGRect nativeRect = new CGRect(
screenRect.X, screenRect.Y,
screenRect.Width, screenRect.Height
);
if (self.AccessibilityFrame != nativeRect)
{
self.AccessibilityFrame = nativeRect;
UIAccessibility.PostNotification(UIAccessibilityPostNotification.LayoutChanged, self);
}
}
private static void UpdateIsReadOnly(AutomationPeerWrapper self)
{
AutomationPeer peer = self;
self.AccessibilityRespondsToUserInteraction =
peer.GetProvider<IValueProvider>()?.IsReadOnly ??
peer.GetProvider<IRangeValueProvider>()?.IsReadOnly ??
peer.IsEnabled();
}
private static void UpdateValue(AutomationPeerWrapper self)
{
AutomationPeer peer = self;
string? newValue =
peer.GetProvider<IRangeValueProvider>()?.Value.ToString("0.##") ??
peer.GetProvider<IValueProvider>()?.Value;
if (self.AccessibilityValue != newValue)
{
self.AccessibilityValue = newValue;
UIAccessibility.PostNotification(UIAccessibilityPostNotification.Announcement, (NSString?)newValue);
}
}
private void PeerChildrenChanged(object? sender, EventArgs e)
{
UpdateChildren();
UIAccessibility.PostNotification(UIAccessibilityPostNotification.ScreenChanged, this);
}
private void PeerPropertyChanged(object? sender, AutomationPropertyChangedEventArgs e) =>
UpdateProperties(e.Property);
private void UpdateProperties(params AutomationProperty[] properties)
{
HashSet<Action<AutomationPeerWrapper>> calledSetters = new();
foreach (AutomationProperty property in properties)
{
if (s_propertySetters.TryGetValue(property,
out Action<AutomationPeerWrapper>? setter) &&
!calledSetters.Contains(setter))
{
calledSetters.Add(setter);
setter.Invoke(this);
}
}
}
public void UpdateAllProperties()
{
UpdateProperties(s_propertySetters.Keys.ToArray());
if (_peer.IsContentElement() && !_peer.IsOffscreen() &&
(_peer.GetName().Length > 0 || _peer.IsKeyboardFocusable()))
{
AccessibilityContainerType = UIAccessibilityContainerType.None;
IsAccessibilityElement = true;
}
else if (_peer.IsOffscreen())
{
AccessibilityContainerType = UIAccessibilityContainerType.None;
IsAccessibilityElement = false;
}
else
{
AccessibilityContainerType = UIAccessibilityContainerType.SemanticGroup;
IsAccessibilityElement = false;
}
}
public void UpdateTraits()
{
UIAccessibilityTrait traits = UIAccessibilityTrait.None;
switch (_peer.GetAutomationControlType())
{
case AutomationControlType.Button:
traits |= UIAccessibilityTrait.Button;
break;
case AutomationControlType.Header:
traits |= UIAccessibilityTrait.Header;
break;
case AutomationControlType.Hyperlink:
traits |= UIAccessibilityTrait.Link;
break;
case AutomationControlType.Image:
traits |= UIAccessibilityTrait.Image;
break;
}
if (_peer.GetProvider<IRangeValueProvider>()?.IsReadOnly == false)
{
traits |= UIAccessibilityTrait.Adjustable;
}
if (_peer.GetProvider<ISelectionItemProvider>()?.IsSelected == true)
{
traits |= UIAccessibilityTrait.Selected;
}
if (_peer.GetProvider<IValueProvider>()?.IsReadOnly == false)
{
traits |= UIAccessibilityTrait.UpdatesFrequently;
}
if (_peer.IsEnabled() == false)
{
traits |= UIAccessibilityTrait.NotEnabled;
}
AccessibilityTraits = (ulong)traits;
}
[Export("accessibilityActivate")]
public bool AccessibilityActivate()
{
IToggleProvider? toggleProvider = _peer.GetProvider<IToggleProvider>();
IInvokeProvider? invokeProvider = _peer.GetProvider<IInvokeProvider>();
if (toggleProvider is not null)
{
toggleProvider.Toggle();
return true;
}
else if (invokeProvider is not null)
{
invokeProvider.Invoke();
return true;
}
else
{
return false;
}
}
public override bool AccessibilityElementIsFocused()
{
base.AccessibilityElementIsFocused();
return _peer.HasKeyboardFocus();
}
public override void AccessibilityElementDidBecomeFocused()
{
base.AccessibilityElementDidBecomeFocused();
_peer.BringIntoView();
}
public override void AccessibilityDecrement()
{
base.AccessibilityDecrement();
IRangeValueProvider? provider = _peer.GetProvider<IRangeValueProvider>();
if (provider is not null)
{
double value = provider.Value;
provider.SetValue(value - provider.SmallChange);
}
}
public override void AccessibilityIncrement()
{
base.AccessibilityIncrement();
IRangeValueProvider? provider = _peer.GetProvider<IRangeValueProvider>();
if (provider is not null)
{
double value = provider.Value;
provider.SetValue(value + provider.SmallChange);
}
}
public override bool AccessibilityScroll(UIAccessibilityScrollDirection direction)
{
base.AccessibilityScroll(direction);
IScrollProvider? scrollProvider = _peer.GetProvider<IScrollProvider>();
if (scrollProvider is not null)
{
bool didScroll;
ScrollAmount verticalAmount, horizontalAmount;
switch (direction)
{
case UIAccessibilityScrollDirection.Up:
verticalAmount = ScrollAmount.LargeIncrement;
horizontalAmount = ScrollAmount.NoAmount;
didScroll = true;
break;
case UIAccessibilityScrollDirection.Down:
verticalAmount = ScrollAmount.LargeDecrement;
horizontalAmount = ScrollAmount.NoAmount;
didScroll = true;
break;
case UIAccessibilityScrollDirection.Left:
verticalAmount = ScrollAmount.NoAmount;
horizontalAmount = ScrollAmount.LargeIncrement;
didScroll = true;
break;
case UIAccessibilityScrollDirection.Right:
verticalAmount = ScrollAmount.NoAmount;
horizontalAmount = ScrollAmount.LargeDecrement;
didScroll = true;
break;
default:
verticalAmount = ScrollAmount.NoAmount;
horizontalAmount = ScrollAmount.NoAmount;
didScroll = false;
break;
}
try
{
scrollProvider.Scroll(verticalAmount, horizontalAmount);
if (didScroll)
{
UIAccessibility.PostNotification(UIAccessibilityPostNotification.PageScrolled, null);
return true;
}
}
catch (InvalidOperationException) { }
}
return false;
}
public static implicit operator AutomationPeer(AutomationPeerWrapper instance)
{
return instance._peer;
}
}
}