using System; using System.Collections.Generic; using System.Linq; using Avalonia.Controls; using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Automation.Peers { /// /// An automation peer which represents a element. /// public class ControlAutomationPeer : AutomationPeer { private IReadOnlyList? _children; private bool _childrenValid; private AutomationPeer? _parent; private bool _parentValid; public ControlAutomationPeer(Control owner) { Owner = owner ?? throw new ArgumentNullException(nameof(owner)); Initialize(); } public Control Owner { get; } public AutomationPeer GetOrCreate(Control element) { if (element == Owner) return this; return CreatePeerForElement(element); } /// /// Gets the for a , creating it if /// necessary. /// /// The control. /// The automation peer. /// /// Despite the name (which comes from the analogous WPF API), this method does not create /// a new peer if one already exists: instead it returns the existing peer. /// public static AutomationPeer CreatePeerForElement(Control element) { return element.GetOrCreateAutomationPeer(); } /// /// Gets an existing for a . /// /// The control. /// The automation peer if already created; otherwise null. /// /// To ensure that a peer is created, use . /// public static AutomationPeer? FromElement(Control element) => element.GetAutomationPeer(); protected override void BringIntoViewCore() => Owner.BringIntoView(); protected override IReadOnlyList GetOrCreateChildrenCore() { var children = _children ?? Array.Empty(); if (_childrenValid) return children; var newChildren = GetChildrenCore() ?? Array.Empty(); foreach (var peer in children.Except(newChildren)) peer.TrySetParent(null); foreach (var peer in newChildren) peer.TrySetParent(this); _childrenValid = true; return _children = newChildren; } protected virtual IReadOnlyList? GetChildrenCore() { var children = Owner.VisualChildren; if (children.Count == 0) return null; var result = new List(); foreach (var child in children) { if (child is Control c) { var peer = GetOrCreate(c); if (c.IsVisible) result.Add(peer); } } return result; } protected override AutomationPeer? GetLabeledByCore() { var label = AutomationProperties.GetLabeledBy(Owner); return label is Control c ? GetOrCreate(c) : null; } protected override string? GetNameCore() { var result = AutomationProperties.GetName(Owner); if (string.IsNullOrWhiteSpace(result) && GetLabeledBy() is AutomationPeer labeledBy) { result = labeledBy.GetName(); } return result; } protected override string? GetHelpTextCore() { var result = AutomationProperties.GetHelpText(Owner); if (string.IsNullOrWhiteSpace(result)) { var errors = DataValidationErrors.GetErrors(Owner); var errorsStringList = errors?.Select(x => x.ToString()); result = errorsStringList != null ? string.Join(Environment.NewLine, errorsStringList.ToArray()) : null; } if (string.IsNullOrWhiteSpace(result)) { result = ToolTip.GetTip(Owner) as string; } return result; } protected override AutomationLandmarkType? GetLandmarkTypeCore() => AutomationProperties.GetLandmarkType(Owner); protected override int GetHeadingLevelCore() => AutomationProperties.GetHeadingLevel(Owner); protected override AutomationPeer? GetParentCore() { EnsureConnected(); return _parent; } protected override AutomationPeer? GetVisualRootCore() { if (Owner.GetVisualRoot() is Control c) return CreatePeerForElement(c); return null; } /// /// Invalidates the peer's children and causes a re-read from . /// protected void InvalidateChildren() { _childrenValid = false; RaiseChildrenChangedEvent(); } /// /// Invalidates the peer's parent. /// protected void InvalidateParent() { _parent = null; _parentValid = false; } protected override bool ShowContextMenuCore() { var c = Owner; while (c is object) { if (c.ContextMenu is object) { c.ContextMenu.Open(c); return true; } c = c.Parent as Control; } return false; } protected internal override bool TrySetParent(AutomationPeer? parent) { _parent = parent; return true; } protected override string? GetAcceleratorKeyCore() => AutomationProperties.GetAcceleratorKey(Owner); protected override string? GetAccessKeyCore() => AutomationProperties.GetAccessKey(Owner); protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Custom; protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner) ?? Owner.Name; protected override Rect GetBoundingRectangleCore() => GetBounds(Owner); protected override string GetClassNameCore() => Owner.GetType().Name; protected override bool HasKeyboardFocusCore() => Owner.IsFocused; protected override bool IsContentElementCore() => true; protected override bool IsControlElementCore() => true; protected override bool IsEnabledCore() => Owner.IsEffectivelyEnabled; protected override bool IsKeyboardFocusableCore() => Owner.Focusable; protected override void SetFocusCore() => Owner.Focus(); protected override AutomationControlType GetControlTypeOverrideCore() { return AutomationProperties.GetControlTypeOverride(Owner) ?? GetAutomationControlTypeCore(); } protected override bool IsContentElementOverrideCore() { var view = AutomationProperties.GetAccessibilityView(Owner); return view == AccessibilityView.Default ? IsContentElementCore() : view >= AccessibilityView.Content; } protected override bool IsControlElementOverrideCore() { var view = AutomationProperties.GetAccessibilityView(Owner); return view == AccessibilityView.Default ? IsControlElementCore() : view >= AccessibilityView.Control; } protected override bool IsOffscreenCore() { return AutomationProperties.GetIsOffscreenBehavior(Owner) switch { IsOffscreenBehavior.Onscreen => false, IsOffscreenBehavior.Offscreen => true, IsOffscreenBehavior.FromClip => Owner.GetTransformedBounds() is not { } bounds || MathUtilities.IsZero(bounds.Clip.Width) || MathUtilities.IsZero(bounds.Clip.Height), _ => !Owner.IsVisible, }; } private static Rect GetBounds(Control control) { var root = control.GetVisualRoot(); if (root is not Visual rootVisual) return default; var transform = control.TransformToVisual(rootVisual); if (!transform.HasValue) return default; return new Rect(control.Bounds.Size).TransformToAABB(transform.Value); } private void Initialize() { Owner.PropertyChanged += OwnerPropertyChanged; var visualChildren = Owner.VisualChildren; visualChildren.CollectionChanged += VisualChildrenChanged; } private void VisualChildrenChanged(object? sender, EventArgs e) => InvalidateChildren(); private void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property == Visual.IsVisibleProperty) { var parent = Owner.GetVisualParent(); if (parent is Control c) (GetOrCreate(c) as ControlAutomationPeer)?.InvalidateChildren(); } else if (e.Property == Visual.BoundsProperty || e.Property == Visual.RenderTransformProperty || e.Property == Visual.RenderTransformOriginProperty) { RaisePropertyChangedEvent( AutomationElementIdentifiers.BoundingRectangleProperty, null, GetBounds(Owner)); } else if (e.Property == Visual.VisualParentProperty) { InvalidateParent(); } } private void EnsureConnected() { if (!_parentValid) { var parent = Owner.GetVisualParent(); while (parent is object) { if (parent is Control c) { var parentPeer = GetOrCreate(c); parentPeer.GetChildren(); } parent = parent.GetVisualParent(); } _parentValid = true; } } } }