diff --git a/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs index 64727c43c5..278e6cdfce 100644 --- a/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs @@ -1,4 +1,5 @@ -using Avalonia.Automation.Provider; +using System.Collections.Generic; +using Avalonia.Automation.Provider; using Avalonia.Controls; namespace Avalonia.Automation.Peers diff --git a/src/Avalonia.Controls/Automation/Peers/ListBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ListBoxAutomationPeer.cs new file mode 100644 index 0000000000..d8aae6f82c --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ListBoxAutomationPeer.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers; + +public class ListBoxAutomationPeer : SelectingItemsControlAutomationPeer +{ + public ListBoxAutomationPeer(ListBox owner) + : base(owner) + { + } +} diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 3ef75f4990..9307bf9b44 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Drawing; using System.Linq; +using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Selection; @@ -126,6 +127,11 @@ namespace Avalonia.Controls return NeedsContainer(item, out recycleKey); } + protected override AutomationPeer OnCreateAutomationPeer() + { + return new ListBoxAutomationPeer(this); + } + protected override void OnKeyDown(KeyEventArgs e) { var hotkeys = this.GetPlatformSettings()?.HotkeyConfiguration; diff --git a/src/Avalonia.FreeDesktop.AtSpi/AtSpiNode.cs b/src/Avalonia.FreeDesktop.AtSpi/AtSpiNode.cs index dc099464a6..09fec39c0f 100644 --- a/src/Avalonia.FreeDesktop.AtSpi/AtSpiNode.cs +++ b/src/Avalonia.FreeDesktop.AtSpi/AtSpiNode.cs @@ -164,6 +164,30 @@ namespace Avalonia.FreeDesktop.AtSpi internal bool RemoveAttachedChild(AtSpiNode child) => _attachedChildren.Remove(child); + // Present a selection container's realized item containers as its direct + // AT-SPI children so SelectChild-by-index and parent lookups work. + private IReadOnlyList GetChildPeers() + { + if (Peer.GetProvider() is null) + return Peer.GetChildren(); + + var items = new List(); + CollectSelectionItemPeers(Peer.GetChildren(), items); + return items.Count > 0 ? items : Peer.GetChildren(); + } + + private static void CollectSelectionItemPeers( + IReadOnlyList peers, List result) + { + foreach (var peer in peers) + { + if (peer.GetProvider() is not null) + result.Add(peer); + else + CollectSelectionItemPeers(peer.GetChildren(), result); + } + } + internal IReadOnlyList EnsureChildren() { if (!IsAttached) @@ -172,7 +196,7 @@ namespace Avalonia.FreeDesktop.AtSpi if (!_childrenDirty) return _attachedChildren; - var childPeers = Peer.GetChildren(); + var childPeers = GetChildPeers(); var nextChildren = new List(childPeers.Count); var nextChildrenSet = new HashSet(); foreach (var childPeer in childPeers) @@ -275,7 +299,7 @@ namespace Avalonia.FreeDesktop.AtSpi _childrenDirty = true; - var childPeers = Peer.GetChildren(); + var childPeers = GetChildPeers(); if (_attachedChildren.Count > 0) { var currentPeers = new HashSet(childPeers); diff --git a/src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiSelectionHandler.cs b/src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiSelectionHandler.cs index 95bd6e032d..85d76374c5 100644 --- a/src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiSelectionHandler.cs +++ b/src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiSelectionHandler.cs @@ -1,6 +1,6 @@ -using System; using System.Collections.Generic; using System.Threading.Tasks; +using Avalonia.Automation.Peers; using Avalonia.Automation.Provider; using Avalonia.DBus; using Avalonia.FreeDesktop.AtSpi.DBusXml; @@ -8,9 +8,6 @@ using static Avalonia.FreeDesktop.AtSpi.AtSpiConstants; namespace Avalonia.FreeDesktop.AtSpi.Handlers { - /// - /// Implements the AT-SPI Selection interface for list-like containers. - /// internal sealed class AtSpiSelectionHandler(AtSpiServer server, AtSpiNode node) : IOrgA11yAtspiSelection { public uint Version => SelectionVersion; @@ -38,7 +35,7 @@ namespace Avalonia.FreeDesktop.AtSpi.Handlers var selectedPeer = selection[selectedChildIndex]; var childNode = server.TryGetAttachedNode(selectedPeer); - if (childNode is null || !ReferenceEquals(childNode.Parent, node)) + if (childNode is null) return ValueTask.FromResult(server.GetNullReference()); return ValueTask.FromResult(server.GetReference(childNode)); @@ -46,15 +43,11 @@ namespace Avalonia.FreeDesktop.AtSpi.Handlers public ValueTask SelectChildAsync(int childIndex) { - var children = node.Peer.GetChildren(); - if (childIndex < 0 || childIndex >= children.Count) + var items = CollectSelectableItems(node.Peer); + if (childIndex < 0 || childIndex >= items.Count) return ValueTask.FromResult(false); - var childPeer = children[childIndex]; - if (childPeer.GetProvider() is not { } selectionItem) - return ValueTask.FromResult(false); - - selectionItem.AddToSelection(); + items[childIndex].AddToSelection(); return ValueTask.FromResult(true); } @@ -78,15 +71,11 @@ namespace Avalonia.FreeDesktop.AtSpi.Handlers public ValueTask IsChildSelectedAsync(int childIndex) { - var children = node.Peer.GetChildren(); - if (childIndex < 0 || childIndex >= children.Count) + var items = CollectSelectableItems(node.Peer); + if (childIndex < 0 || childIndex >= items.Count) return ValueTask.FromResult(false); - var childPeer = children[childIndex]; - if (childPeer.GetProvider() is not { } selectionItem) - return ValueTask.FromResult(false); - - return ValueTask.FromResult(selectionItem.IsSelected); + return ValueTask.FromResult(items[childIndex].IsSelected); } public ValueTask SelectAllAsync() @@ -95,12 +84,8 @@ namespace Avalonia.FreeDesktop.AtSpi.Handlers if (provider is null || !provider.CanSelectMultiple) return ValueTask.FromResult(false); - var children = node.Peer.GetChildren(); - foreach (var child in children) - { - if (child.GetProvider() is { } selectionItem) - selectionItem.AddToSelection(); - } + foreach (var item in CollectSelectableItems(node.Peer)) + item.AddToSelection(); return ValueTask.FromResult(true); } @@ -123,16 +108,32 @@ namespace Avalonia.FreeDesktop.AtSpi.Handlers public ValueTask DeselectChildAsync(int childIndex) { - var children = node.Peer.GetChildren(); - if (childIndex < 0 || childIndex >= children.Count) + var items = CollectSelectableItems(node.Peer); + if (childIndex < 0 || childIndex >= items.Count) return ValueTask.FromResult(false); - var childPeer = children[childIndex]; - if (childPeer.GetProvider() is not { } selectionItem) - return ValueTask.FromResult(false); - - selectionItem.RemoveFromSelection(); + items[childIndex].RemoveFromSelection(); return ValueTask.FromResult(true); } + + private static List CollectSelectableItems(AutomationPeer peer) + { + var result = new List(); + CollectSelectableItemsCore(peer.GetChildren(), result); + return result; + } + + private static void CollectSelectableItemsCore( + IReadOnlyList children, + List result) + { + foreach (var child in children) + { + if (child.GetProvider() is { } item) + result.Add(item); + else + CollectSelectableItemsCore(child.GetChildren(), result); + } + } } }