Skip to content
Draft
52 changes: 51 additions & 1 deletion native/Avalonia.Native/src/OSX/automation.mm
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ - (NSAccessibilitySubrole)accessibilitySubrole

auto landmarkType = _peer->GetLandmarkType();
switch (landmarkType) {
case LandmarkNone: break;
case LandmarkBanner: return @"AXLandmarkBanner";
case LandmarkComplementary: return @"AXLandmarkComplementary";
case LandmarkContentInfo: return @"AXLandmarkContentInfo";
Expand All @@ -161,6 +162,7 @@ - (NSString *)accessibilityRoleDescription
{
auto landmarkType = _peer->GetLandmarkType();
switch (landmarkType) {
case LandmarkNone: break;
case LandmarkBanner: return @"banner";
case LandmarkComplementary: return @"complementary";
case LandmarkContentInfo: return @"content";
Expand Down Expand Up @@ -191,6 +193,7 @@ - (id)accessibilityAttributeValue:(NSAccessibilityAttributeName)attribute
{
switch (_peer->GetLiveSetting())
{
case LiveSettingOff: break;
case LiveSettingPolite: return @"polite";
case LiveSettingAssertive: return @"assertive";
}
Expand Down Expand Up @@ -401,6 +404,49 @@ - (BOOL)isAccessibilitySelected
return NO;
}

- (void)setAccessibilitySelected:(BOOL)accessibilitySelected
{
if (!_peer->IsSelectionItemProvider())
return;
if (accessibilitySelected)
_peer->SelectionItemProvider_AddToSelection();
else
_peer->SelectionItemProvider_RemoveFromSelection();
}

- (BOOL)accessibilityPerformPick
{
if (!_peer->IsSelectionItemProvider())
return NO;
_peer->SelectionItemProvider_Select();
return YES;
}

- (NSArray *)accessibilitySelectedChildren
{
if (!_peer->IsSelectionProvider())
return nil;

auto selection = _peer->SelectionProvider_GetSelection();
if (selection == nullptr)
return nil;
auto count = selection->GetCount();
NSMutableArray* selectedElements = [[NSMutableArray alloc] initWithCapacity:count];

for (int i = 0; i < count; ++i)
{
IAvnAutomationPeer* selectedPeer;

if (selection->Get(i, &selectedPeer) == S_OK)
{
id element = [AvnAccessibilityElement acquire:selectedPeer];
[selectedElements addObject:element];
}
}

return selectedElements;
}

- (BOOL)isAccessibilitySelectorAllowed:(SEL)selector
{
if (selector == @selector(accessibilityPerformShowMenu))
Expand All @@ -422,7 +468,11 @@ - (BOOL)isAccessibilitySelectorAllowed:(SEL)selector
{
return _peer->IsRangeValueProvider();
}

else if (selector == @selector(accessibilityPerformPick))
{
return _peer->IsSelectionItemProvider();
}

return [super isAccessibilitySelectorAllowed:selector];
}

Expand Down
142 changes: 142 additions & 0 deletions src/Avalonia.Controls/Automation/Peers/ListBoxAutomationPeer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Automation.Provider;
using Avalonia.Controls;
using Avalonia.Controls.Selection;
using Avalonia.Controls.Utils;

namespace Avalonia.Automation.Peers
{
public class ListBoxAutomationPeer : SelectingItemsControlAutomationPeer
{
public ListBoxAutomationPeer(ListBox owner)
: base(owner)
{
owner.ContainerPrepared += OnContainerPrepared;
}

private void OnContainerPrepared(object? sender, ContainerPreparedEventArgs e)
{
if (e.Container.GetAutomationPeer() is ListItemAutomationPeer peer)
peer.Index = e.Index;
}

protected override void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
base.OwnerPropertyChanged(sender, e);

if (e.Property == ItemsControl.ItemCountProperty)
InvalidateChildren();
}

public new ListBox Owner => (ListBox)base.Owner;

protected override IReadOnlyList<AutomationPeer>? GetChildrenCore()
{
if (Owner.ItemCount == 0)
{
return null;
}

var children = new List<AutomationPeer>();

for (var i = 0; i < Owner.ItemCount; i++)
{
var container = Owner.ContainerFromIndex(i);

if (container == null)
{
children.Add(new UnrealizedListItemAutomationPeer(this, i));
}
else
{
var peer = GetOrCreate(container);
if (peer is ListItemAutomationPeer listItemPeer)
listItemPeer.Index = i;
children.Add(peer);
}
}

return children;
}

internal class UnrealizedListItemAutomationPeer(ListBoxAutomationPeer owner, int index)
: UnrealizedElementAutomationPeer, ISelectionItemProvider
{
private ListBox _listBox => owner.Owner;

public bool IsSelected => _listBox.Selection.SelectedIndexes.Contains(index);

public ISelectionProvider? SelectionContainer => owner.GetProvider<ISelectionProvider>();

public void Select()
{
EnsureEnabled();
BringIntoView();

_listBox.SelectedIndex = index;
}

public void AddToSelection()
{
EnsureEnabled();
BringIntoView();

if (_listBox.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel)
{
selectionModel.Select(index);
}
}

public void RemoveFromSelection()
{
EnsureEnabled();
BringIntoView();

if (_listBox.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel)
{
selectionModel.Deselect(index);
}
}

protected override void BringIntoViewCore()
{
var container = _listBox.ContainerFromIndex(index);
container?.BringIntoView();
}

protected override string? GetAcceleratorKeyCore() => null;
protected override string? GetAccessKeyCore() => null;
protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.ListItem;
protected override string GetAutomationIdCore() => $"{nameof(ListBoxItem)}: {index}";

protected override Rect GetBoundingRectangleCore()
{
var container = _listBox.ContainerFromIndex(index);
var peer = container?.GetOrCreateAutomationPeer();
return peer?.GetBoundingRectangle() ?? base.GetBoundingRectangleCore();
}

protected override string GetClassNameCore() => nameof(ListBoxItem);
protected override AutomationPeer? GetLabeledByCore() => null;

protected override string? GetNameCore()
{
using var textBindingEvaluator = BindingEvaluator<string?>.TryCreate(_listBox.DisplayMemberBinding);
var item = _listBox.Items.ElementAtOrDefault(index);
return textBindingEvaluator?.Evaluate(item);
}

protected override AutomationPeer? GetParentCore() => owner;
protected override bool IsContentElementCore() => true;
protected override bool IsControlElementCore() => true;
protected override bool IsEnabledCore() => _listBox.IsEnabled;
protected override bool IsKeyboardFocusableCore() => true;

protected override void SetFocusCore()
{
BringIntoView();
}
}
}
}
47 changes: 42 additions & 5 deletions src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ namespace Avalonia.Automation.Peers
public class ListItemAutomationPeer : ContentControlAutomationPeer,
ISelectionItemProvider
{
private int? _index;

public int Index
{
get
{
if (_index.HasValue)
return _index.Value;

if (Owner.Parent is ItemsControl parent)
return parent.IndexFromContainer(Owner);

return -1;
}

set => _index = value;
}

public ListItemAutomationPeer(ContentControl owner)
: base(owner)
{
Expand All @@ -32,38 +50,41 @@ public ISelectionProvider? SelectionContainer
public void Select()
{
EnsureEnabled();
BringIntoView();

if (Owner.Parent is SelectingItemsControl parent)
{
var index = parent.IndexFromContainer(Owner);
var index = Index;

if (index != -1)
parent.SelectedIndex = index;
}
}

void ISelectionItemProvider.AddToSelection()
public void AddToSelection()
{
EnsureEnabled();
BringIntoView();

if (Owner.Parent is ItemsControl parent &&
parent.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel)
{
var index = parent.IndexFromContainer(Owner);
var index = Index;

if (index != -1)
selectionModel.Select(index);
}
}

void ISelectionItemProvider.RemoveFromSelection()
public void RemoveFromSelection()
{
EnsureEnabled();
BringIntoView();

if (Owner.Parent is ItemsControl parent &&
parent.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel)
{
var index = parent.IndexFromContainer(Owner);
var index = Index;

if (index != -1)
selectionModel.Deselect(index);
Expand All @@ -75,7 +96,23 @@ protected override AutomationControlType GetAutomationControlTypeCore()
return AutomationControlType.ListItem;
}

protected override string? GetAutomationIdCore()
{
var index = Index;

if (index == -1)
return base.GetAutomationIdCore();

return $"{nameof(ListBoxItem)}: {index}";
}

protected override bool IsContentElementCore() => true;
protected override bool IsControlElementCore() => true;

protected override void SetFocusCore()
{
base.SetFocusCore();
BringIntoView();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,7 @@ public interface ISelectionItemProvider
/// </item>
/// <item>
/// <term>macOS</term>
/// <description>
/// <c>NSAccessibilityProtocol.accessibilityPerformPick</c> (not implemented).
/// <c>NSAccessibilityProtocol.setAccessibilitySelected</c> (not implemented).
/// </description>
/// <description><c>NSAccessibilityProtocol.setAccessibilitySelected</c></description>
/// </item>
/// </list>
/// </remarks>
Expand All @@ -72,9 +69,7 @@ public interface ISelectionItemProvider
/// </item>
/// <item>
/// <term>macOS</term>
/// <description>
/// <c>NSAccessibilityProtocol.setAccessibilitySelected</c> (not implemented).
/// </description>
/// <description><c>NSAccessibilityProtocol.setAccessibilitySelected</c></description>
/// </item>
/// </list>
/// </remarks>
Expand All @@ -91,7 +86,7 @@ public interface ISelectionItemProvider
/// </item>
/// <item>
/// <term>macOS</term>
/// <description>No mapping.</description>
/// <description><c>NSAccessibilityProtocol.accessibilityPerformPick</c></description>
/// </item>
/// </list>
/// </remarks>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,7 @@ public interface ISelectionProvider
/// </item>
/// <item>
/// <term>macOS</term>
/// <description>
/// <c>NSAccessibilityProtocol.accessibilitySelectedChildren</c> (not implemented).
/// </description>
/// <description><c>NSAccessibilityProtocol.accessibilitySelectedChildren</c></description>
/// </item>
/// </list>
/// </remarks>
Expand Down
6 changes: 6 additions & 0 deletions src/Avalonia.Controls/ListBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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;
Expand Down Expand Up @@ -155,5 +156,10 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
base.OnApplyTemplate(e);
Scroll = e.NameScope.Find<IScrollable>("PART_ScrollViewer");
}

protected override AutomationPeer OnCreateAutomationPeer()
{
return new ListBoxAutomationPeer(this);
}
}
}
Loading
Loading