Browse Source

AT-SPI: Expose list item containers as accessible children (#21605)

* AT-SPI: add to the selection in Selection.SelectChild instead of replacing it

* AT-SPI: expose list item containers as accessible children

Index selectable items directly under the selection container so the
Selection interface's SelectChild/DeselectChild address them by index.

* Expose ListBox selection to accessibility via a selection peer

ListBox used ItemsControlAutomationPeer, which is not an ISelectionProvider,
so it never exposed the Selection interface (AT-SPI) for multi-select.

* Address review: expose item containers from the base ItemsControl peer

Move the GetChildrenCore container override down to ItemsControlAutomationPeer so every ItemsControl returns its containers, and return null directly when there are no items instead of calling the base algorithm.

* Use file-scoped namespace in ListBoxAutomationPeer

* Fix cross-platform regression: move child flattening from the peer to the AT-SPI selection handler

* Trigger rebuild

* AT-SPI: flatten selection container item children in the node layer, not the shared peer

Present a SelectingItemsControl's realized item containers as its direct AT-SPI children inside AtSpiNode so SelectChild-by-index and a selectable item's parent lookup resolve to the Selection container. Doing this in the FreeDesktop AT-SPI layer keeps the macOS AX and Windows UIA peer trees intact, avoiding the item-frame and tree-collapse regressions the shared ItemsControlAutomationPeer override caused.
pull/21634/head
Jumar Macato 3 months ago
committed by GitHub
parent
commit
b25f2d98c2
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs
  2. 11
      src/Avalonia.Controls/Automation/Peers/ListBoxAutomationPeer.cs
  3. 6
      src/Avalonia.Controls/ListBox.cs
  4. 28
      src/Avalonia.FreeDesktop.AtSpi/AtSpiNode.cs
  5. 65
      src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiSelectionHandler.cs

3
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

11
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)
{
}
}

6
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<ListBoxItem>(item, out recycleKey);
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new ListBoxAutomationPeer(this);
}
protected override void OnKeyDown(KeyEventArgs e)
{
var hotkeys = this.GetPlatformSettings()?.HotkeyConfiguration;

28
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<AutomationPeer> GetChildPeers()
{
if (Peer.GetProvider<ISelectionProvider>() is null)
return Peer.GetChildren();
var items = new List<AutomationPeer>();
CollectSelectionItemPeers(Peer.GetChildren(), items);
return items.Count > 0 ? items : Peer.GetChildren();
}
private static void CollectSelectionItemPeers(
IReadOnlyList<AutomationPeer> peers, List<AutomationPeer> result)
{
foreach (var peer in peers)
{
if (peer.GetProvider<ISelectionItemProvider>() is not null)
result.Add(peer);
else
CollectSelectionItemPeers(peer.GetChildren(), result);
}
}
internal IReadOnlyList<AtSpiNode> 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<AtSpiNode>(childPeers.Count);
var nextChildrenSet = new HashSet<AtSpiNode>();
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<AutomationPeer>(childPeers);

65
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
{
/// <summary>
/// Implements the AT-SPI Selection interface for list-like containers.
/// </summary>
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<bool> 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<ISelectionItemProvider>() 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<bool> 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<ISelectionItemProvider>() is not { } selectionItem)
return ValueTask.FromResult(false);
return ValueTask.FromResult(selectionItem.IsSelected);
return ValueTask.FromResult(items[childIndex].IsSelected);
}
public ValueTask<bool> 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<ISelectionItemProvider>() 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<bool> 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<ISelectionItemProvider>() is not { } selectionItem)
return ValueTask.FromResult(false);
selectionItem.RemoveFromSelection();
items[childIndex].RemoveFromSelection();
return ValueTask.FromResult(true);
}
private static List<ISelectionItemProvider> CollectSelectableItems(AutomationPeer peer)
{
var result = new List<ISelectionItemProvider>();
CollectSelectableItemsCore(peer.GetChildren(), result);
return result;
}
private static void CollectSelectableItemsCore(
IReadOnlyList<AutomationPeer> children,
List<ISelectionItemProvider> result)
{
foreach (var child in children)
{
if (child.GetProvider<ISelectionItemProvider>() is { } item)
result.Add(item);
else
CollectSelectableItemsCore(child.GetChildren(), result);
}
}
}
}

Loading…
Cancel
Save