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 { internal class AutomationPeerWrapper : UIAccessibilityElement { private static readonly IReadOnlyDictionary> s_propertySetters = new Dictionary>() { { 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; public AutomationPeerWrapper(AvaloniaView view, AutomationPeer? peer = null) : base(view) { _view = view; _peer = peer ?? ControlAutomationPeer.CreatePeerForElement(view.TopLevel); _peer.PropertyChanged += PeerPropertyChanged; _peer.ChildrenChanged += PeerChildrenChanged; AccessibilityContainer = _view; AccessibilityIdentifier = _peer.GetAutomationId(); } 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, null); } } private static void UpdateIsReadOnly(AutomationPeerWrapper self) { AutomationPeer peer = self; self.AccessibilityRespondsToUserInteraction = peer.GetProvider()?.IsReadOnly ?? peer.GetProvider()?.IsReadOnly ?? peer.IsEnabled(); } private static void UpdateValue(AutomationPeerWrapper self) { AutomationPeer peer = self; string? newValue = peer.GetProvider()?.Value.ToString("0.##") ?? peer.GetProvider()?.Value; if (self.AccessibilityValue != newValue) { self.AccessibilityValue = newValue; UIAccessibility.PostNotification(UIAccessibilityPostNotification.Announcement, null); } } private void PeerChildrenChanged(object? sender, EventArgs e) => UIAccessibility.PostNotification(UIAccessibilityPostNotification.ScreenChanged, null); private void PeerPropertyChanged(object? sender, AutomationPropertyChangedEventArgs e) => UpdateProperties(e.Property); private void UpdateProperties(params AutomationProperty[] properties) { HashSet> calledSetters = new(); foreach (AutomationProperty property in properties) { if (s_propertySetters.TryGetValue(property, out Action? setter) && !calledSetters.Contains(setter)) { calledSetters.Add(setter); setter.Invoke(this); } } } public bool UpdatePropertiesIfValid() { if (_peer.IsContentElement() && !_peer.IsOffscreen()) { UpdateProperties(s_propertySetters.Keys.ToArray()); return IsAccessibilityElement = true; } else { return 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()?.IsReadOnly == false) { traits |= UIAccessibilityTrait.Adjustable; } if (_peer.GetProvider()?.IsSelected == true) { traits |= UIAccessibilityTrait.Selected; } if (_peer.GetProvider()?.IsReadOnly == false) { traits |= UIAccessibilityTrait.UpdatesFrequently; } if (_peer.IsEnabled() == false) { traits |= UIAccessibilityTrait.NotEnabled; } AccessibilityTraits = (ulong)traits; } [Export("accessibilityActivate")] public bool AccessibilityActivate() { IToggleProvider? toggleProvider = _peer.GetProvider(); IInvokeProvider? invokeProvider = _peer.GetProvider(); 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(); if (provider is not null) { double value = provider.Value; provider.SetValue(value - provider.SmallChange); } } public override void AccessibilityIncrement() { base.AccessibilityIncrement(); IRangeValueProvider? provider = _peer.GetProvider(); 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(); if (scrollProvider is not null) { bool didScroll; ScrollAmount verticalAmount, horizontalAmount; switch (direction) { case UIAccessibilityScrollDirection.Up: verticalAmount = ScrollAmount.SmallIncrement; horizontalAmount = ScrollAmount.NoAmount; didScroll = true; break; case UIAccessibilityScrollDirection.Down: verticalAmount = ScrollAmount.SmallDecrement; horizontalAmount = ScrollAmount.NoAmount; didScroll = true; break; case UIAccessibilityScrollDirection.Left: verticalAmount = ScrollAmount.NoAmount; horizontalAmount = ScrollAmount.SmallIncrement; didScroll = true; break; case UIAccessibilityScrollDirection.Right: verticalAmount = ScrollAmount.NoAmount; horizontalAmount = ScrollAmount.SmallDecrement; didScroll = true; break; default: verticalAmount = ScrollAmount.NoAmount; horizontalAmount = ScrollAmount.NoAmount; didScroll = false; break; } scrollProvider.Scroll(verticalAmount, horizontalAmount); if (didScroll) { UIAccessibility.PostNotification(UIAccessibilityPostNotification.PageScrolled, null); return true; } } return false; } public static implicit operator AutomationPeer(AutomationPeerWrapper instance) { return instance._peer; } } }