Browse Source

Implemented AutomationProperties.LiveSetting (#20473)

* Implemented LiveSetting property

* Make sure PropertyChanged callback is called on AvnAutomationPeer

* Remove `optional` from `AvnAccessibility`: we were never checking whether they respond to the selector or not anyway

* Formatting

* Use ARIA and Live Region Changed constants from mac APIs

* Fixed Mac build

* Reverted constants that don't exist on integration test XCode version

---------

Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
pull/20561/head
Melissa 2 weeks ago
committed by GitHub
parent
commit
b775804b61
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      native/Avalonia.Native/src/OSX/AvnAccessibility.h
  2. 4
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  3. 36
      native/Avalonia.Native/src/OSX/automation.mm
  4. 3
      samples/IntegrationTestApp/Pages/AutomationPage.axaml
  5. 6
      samples/IntegrationTestApp/Pages/AutomationPage.axaml.cs
  6. 2
      src/Avalonia.Controls/Automation/AutomationProperties.cs
  7. 7
      src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
  8. 2
      src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
  9. 10
      src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs
  10. 16
      src/Avalonia.Native/AvnAutomationPeer.cs
  11. 9
      src/Avalonia.Native/avn.idl
  12. 15
      src/Windows/Avalonia.Win32.Automation/AutomationNode.cs
  13. 7
      src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple.cs

1
native/Avalonia.Native/src/OSX/AvnAccessibility.h

@ -7,7 +7,6 @@
@protocol AvnAccessibility <NSAccessibility>
@required
- (void) raiseChildrenChanged;
@optional
- (void) raiseFocusChanged;
- (void) raisePropertyChanged:(AvnAutomationProperty)property;
@end

4
native/Avalonia.Native/src/OSX/AvnWindow.mm

@ -656,5 +656,9 @@
NSAccessibilityPostNotification(focused, NSAccessibilityFocusedUIElementChangedNotification);
}
- (void)raisePropertyChanged:(AvnAutomationProperty)property
{
}
@end

36
native/Avalonia.Native/src/OSX/automation.mm

@ -11,6 +11,7 @@
IAvnAutomationPeer* _peer;
AvnAutomationNode* _node;
NSMutableArray* _children;
NSArray<NSString*>* _attributeNames;
}
+ (NSAccessibilityElement *)acquire:(IAvnAutomationPeer *)peer
@ -166,6 +167,32 @@
return NSAccessibilityRoleDescription([self accessibilityRole], [self accessibilitySubrole]);
}
// Note: Apple has deprecated this API, but it's still used to set attributes not supported by NSAccessibility
- (NSArray<NSString *> *)accessibilityAttributeNames
{
if (_attributeNames == nil)
{
_attributeNames = @[
@"AXARIALive", // kAXARIALiveAttribute
];
}
return _attributeNames;
}
- (id)accessibilityAttributeValue:(NSAccessibilityAttributeName)attribute
{
if ([attribute isEqualToString:@"AXARIALive" /* kAXARIALiveAttribute */])
{
switch (_peer->GetLiveSetting())
{
case LiveSettingPolite: return @"polite";
case LiveSettingAssertive: return @"assertive";
}
return nil;
}
return nil;
}
- (NSString *)accessibilityIdentifier
{
return GetNSStringAndRelease(_peer->GetAutomationId());
@ -421,8 +448,15 @@
@{ NSAccessibilityUIElementsKey: [changed allObjects]});
}
- (void)raisePropertyChanged
- (void)raisePropertyChanged:(AvnAutomationProperty)property
{
if (property == AutomationPeer_Name && _peer->GetLiveSetting() != LiveSettingOff)
[self raiseLiveRegionChanged];
}
- (void)raiseLiveRegionChanged
{
NSAccessibilityPostNotification(self, @"AXLiveRegionChanged" /* kAXLiveRegionChangedNotification */);
}
- (void)setAccessibilityFocused:(BOOL)accessibilityFocused

3
samples/IntegrationTestApp/Pages/AutomationPage.axaml

@ -22,5 +22,8 @@
<TextBlock Name="TextBlockWithoutHeader">
Header None
</TextBlock>
<Button Click="OnButtonAddSomeText">Add some live region text</Button>
<TextBlock Name="textLiveRegion" AutomationProperties.LiveSetting="Assertive">This is an assertive live region.</TextBlock>
</StackPanel>
</UserControl>

6
samples/IntegrationTestApp/Pages/AutomationPage.axaml.cs

@ -1,4 +1,5 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace IntegrationTestApp.Pages;
@ -8,4 +9,9 @@ public partial class AutomationPage : UserControl
{
InitializeComponent();
}
private void OnButtonAddSomeText(object? sender, RoutedEventArgs? e)
{
textLiveRegion.Text += " Lorem ipsum.";
}
}

2
src/Avalonia.Controls/Automation/AutomationProperties.cs

@ -211,7 +211,7 @@ namespace Avalonia.Automation
/// Defines the AutomationProperties.LiveSetting attached property.
/// </summary>
/// <remarks>
/// This property currently has no effect.
/// This property affects the default value for <see cref="AutomationPeer.GetLiveSetting"/> and controls whether live region changed events are emitted.
/// </remarks>
public static readonly AttachedProperty<AutomationLiveSetting> LiveSettingProperty =
AvaloniaProperty.RegisterAttached<StyledElement, AutomationLiveSetting>(

7
src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs

@ -476,6 +476,12 @@ namespace Avalonia.Automation.Peers
/// <returns>true if a context menu is present for the element; otherwise false.</returns>
public bool ShowContextMenu() => ShowContextMenuCore();
/// <summary>
/// Gets the current live setting that is associated with this this automation peer.
/// </summary>
/// <returns>The live setting to use for automation.</returns>
public AutomationLiveSetting GetLiveSetting() => GetLiveSettingCore();
/// <summary>
/// Tries to get a provider of the specified type from the peer.
/// </summary>
@ -565,6 +571,7 @@ namespace Avalonia.Automation.Peers
protected virtual bool IsOffscreenCore() => false;
protected abstract void SetFocusCore();
protected abstract bool ShowContextMenuCore();
protected virtual AutomationLiveSetting GetLiveSettingCore() => AutomationLiveSetting.Off;
protected virtual AutomationControlType GetControlTypeOverrideCore()
{

2
src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs

@ -185,6 +185,8 @@ namespace Avalonia.Automation.Peers
return false;
}
protected override AutomationLiveSetting GetLiveSettingCore() => AutomationProperties.GetLiveSetting(Owner);
protected internal override bool TrySetParent(AutomationPeer? parent)
{
_parent = parent;

10
src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs

@ -7,6 +7,16 @@ namespace Avalonia.Automation.Peers
public TextBlockAutomationPeer(TextBlock owner)
: base(owner)
{
Owner.PropertyChanged += (a, e) =>
{
if (e.Property == TextBlock.TextProperty)
{
RaisePropertyChangedEvent(
AutomationElementIdentifiers.NameProperty,
e.OldValue,
e.NewValue);
}
};
}
public new TextBlock Owner => (TextBlock)base.Owner;

16
src/Avalonia.Native/AvnAutomationPeer.cs

@ -13,6 +13,14 @@ namespace Avalonia.Native
{
internal class AvnAutomationPeer : NativeCallbackBase, IAvnAutomationPeer
{
private static readonly Dictionary<AutomationProperty, AvnAutomationProperty> s_propertyMap = new()
{
{ AutomationElementIdentifiers.BoundingRectangleProperty, AvnAutomationProperty.AutomationPeer_BoundingRectangle },
{ AutomationElementIdentifiers.ClassNameProperty, AvnAutomationProperty.AutomationPeer_ClassName },
{ AutomationElementIdentifiers.NameProperty, AvnAutomationProperty.AutomationPeer_Name },
{ RangeValuePatternIdentifiers.ValueProperty, AvnAutomationProperty.RangeValueProvider_Value },
};
private static readonly ConditionalWeakTable<AutomationPeer, AvnAutomationPeer> s_wrappers = new();
private readonly AutomationPeer _inner;
@ -20,6 +28,7 @@ namespace Avalonia.Native
{
_inner = inner;
_inner.ChildrenChanged += (_, _) => Node?.ChildrenChanged();
_inner.PropertyChanged += OnPeerPropertyChanged;
if (inner is IRootProvider root)
root.FocusChanged += (_, _) => Node?.FocusChanged();
}
@ -41,6 +50,7 @@ namespace Avalonia.Native
public int HeadingLevel => _inner.GetHeadingLevel();
public IAvnAutomationPeer? Parent => Wrap(_inner.GetParent());
public IAvnAutomationPeer? VisualRoot => Wrap(_inner.GetVisualRoot());
public AvnLiveSetting LiveSetting => (AvnLiveSetting)_inner.GetLiveSetting();
public int HasKeyboardFocus() => _inner.HasKeyboardFocus().AsComBool();
public int IsContentElement() => _inner.IsContentElement().AsComBool();
@ -186,6 +196,12 @@ namespace Avalonia.Native
}
private int IsProvider<T>() => (_inner.GetProvider<T>() is not null).AsComBool();
private void OnPeerPropertyChanged(object? sender, AutomationPropertyChangedEventArgs e)
{
if (s_propertyMap.TryGetValue(e.Property, out var property))
Node?.PropertyChanged(property);
}
}
internal class AvnAutomationPeerArray : NativeCallbackBase, IAvnAutomationPeerArray

9
src/Avalonia.Native/avn.idl

@ -689,6 +689,13 @@ enum AvnPointerDeviceType
Pen,
}
enum AvnLiveSetting
{
LiveSettingOff,
LiveSettingPolite,
LiveSettingAssertive,
}
[uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)]
interface IAvaloniaNativeFactory : IUnknown
{
@ -1324,6 +1331,8 @@ interface IAvnAutomationPeer : IUnknown
AvnLandmarkType GetLandmarkType();
int GetHeadingLevel();
AvnLiveSetting GetLiveSetting();
}
[uuid(b00af5da-78af-4b33-bfff-4ce13a6239a9)]

15
src/Windows/Avalonia.Win32.Automation/AutomationNode.cs

@ -141,6 +141,7 @@ namespace Avalonia.Win32.Automation
UiaPropertyId.LandmarkType => InvokeSync(() => ToUiaLandmarkType(Peer.GetLandmarkType())),
UiaPropertyId.LocalizedLandmarkType => InvokeSync(() => ToUiaLocalizedLandmarkType(Peer.GetLandmarkType())),
UiaPropertyId.HeadingLevel => InvokeSync(() => ToUiaHeadingLevel(Peer.GetHeadingLevel())),
UiaPropertyId.LiveSetting => InvokeSync(() => ToUiaLiveSetting(Peer.GetLiveSetting())),
UiaPropertyId.ProcessId => s_pid,
UiaPropertyId.RuntimeId => _runtimeId,
_ => null,
@ -273,6 +274,13 @@ namespace Avalonia.Win32.Automation
(int)UiaEventId.AutomationFocusChanged);
}
protected void RaiseLiveRegionChanged()
{
UiaCoreProviderApi.UiaRaiseAutomationEvent(
this,
(int)UiaEventId.LiveRegionChanged);
}
private RootAutomationNode? GetRoot()
{
Dispatcher.UIThread.VerifyAccess();
@ -294,6 +302,11 @@ namespace Avalonia.Win32.Automation
e.OldValue as IConvertible,
e.NewValue as IConvertible);
}
if (id == UiaPropertyId.Name && Peer.GetLiveSetting() != AutomationLiveSetting.Off)
{
RaiseLiveRegionChanged();
}
}
private void OnEmbeddedRootFocusChanged(object? sender, EventArgs e)
@ -407,6 +420,8 @@ namespace Avalonia.Win32.Automation
};
}
private static UiaLiveSetting ToUiaLiveSetting(AutomationLiveSetting liveSetting) => (UiaLiveSetting)liveSetting;
private static int GetProcessId()
{
#if NET6_0_OR_GREATER

7
src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple.cs

@ -300,6 +300,13 @@ internal enum UiaHeadingLevel
Level9
};
internal enum UiaLiveSetting
{
Off = 0,
Polite,
Assertive,
};
#if NET8_0_OR_GREATER
[GeneratedComInterface]
#else

Loading…
Cancel
Save