From 2a44d8b56463c71e4a1757d43e8fa9b8e762ab98 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 9 Mar 2021 13:25:50 +0100 Subject: [PATCH 01/73] Initial implementation of UI automation. Follows WPF/UWP API as closely as possible. Limited to win32 right now. Broken in many places. --- .../AutomationElementIdentifiers.cs | 28 + .../Automation/AutomationLiveSetting.cs | 28 + .../Automation/AutomationProperties.cs | 539 ++++++++++++++++++ .../Automation/AutomationProperty.cs | 11 + .../Automation/ElementNotEnabledException.cs | 10 + .../ExpandCollapsePatternIdentifiers.cs | 15 + .../Automation/ExpandCollapseState.cs | 29 + .../Automation/IsOffscreenBehavior.cs | 26 + .../Automation/Peers/AutomationPeer.cs | 227 ++++++++ .../Automation/Peers/ButtonAutomationPeer.cs | 29 + .../Peers/CheckBoxAutomationPeer.cs | 20 + .../Peers/ComboBoxAutomationPeer.cs | 123 ++++ .../Peers/ContentControlAutomationPeer.cs | 39 ++ .../Automation/Peers/ControlAutomationPeer.cs | 196 +++++++ .../Automation/Peers/ImageAutomationPeer.cs | 20 + .../Peers/ItemsControlAutomationPeer.cs | 57 ++ .../Peers/ListItemAutomationPeer.cs | 82 +++ .../Automation/Peers/MenuAutomationPeer.cs | 22 + .../Peers/MenuItemAutomationPeer.cs | 39 ++ .../Automation/Peers/NoneAutomationPeer.cs | 28 + .../Peers/PopupRootAutomationPeer.cs | 36 ++ .../Peers/RangeBaseAutomationPeer.cs | 34 ++ .../Peers/ScrollViewerAutomationPeer.cs | 175 ++++++ .../SelectingItemsControlAutomationPeer.cs | 87 +++ .../Automation/Peers/SliderAutomationPeer.cs | 20 + .../Peers/TabControlAutomationPeer.cs | 20 + .../Automation/Peers/TabItemAutomationPeer.cs | 18 + .../Automation/Peers/TextAutomationPeer.cs | 28 + .../Automation/Peers/TextBoxAutomationPeer.cs | 21 + .../Peers/ToggleButtonAutomationPeer.cs | 39 ++ .../Peers/UnrealizedElementAutomationPeer.cs | 32 ++ .../Automation/Peers/WindowAutomationPeer.cs | 39 ++ .../Peers/WindowBaseAutomationPeer.cs | 72 +++ .../Automation/Platform/IAutomationNode.cs | 32 ++ .../Platform/IAutomationNodeFactory.cs | 18 + .../Platform/IRootAutomationNode.cs | 20 + .../Provider/IExpandCollapseProvider.cs | 24 + .../Automation/Provider/IInvokeProvider.cs | 15 + .../Provider/IRangeValueProvider.cs | 37 ++ .../Automation/Provider/IRootProvider.cs | 14 + .../Automation/Provider/IScrollProvider.cs | 71 +++ .../Provider/ISelectionItemProvider .cs | 37 ++ .../Automation/Provider/ISelectionProvider.cs | 29 + .../Automation/Provider/IToggleProvider.cs | 40 ++ .../Automation/Provider/IValueProvider.cs | 31 + .../RangeValuePatternIdentifiers.cs | 30 + .../Automation/ScrollPatternIdentifiers.cs | 45 ++ .../Automation/SelectionPatternIdentifiers.cs | 25 + src/Avalonia.Controls/Button.cs | 9 + src/Avalonia.Controls/CheckBox.cs | 6 + src/Avalonia.Controls/ComboBox.cs | 7 + src/Avalonia.Controls/Control.cs | 21 + src/Avalonia.Controls/Image.cs | 7 + src/Avalonia.Controls/ItemsControl.cs | 7 + src/Avalonia.Controls/ListBoxItem.cs | 8 +- src/Avalonia.Controls/Menu.cs | 7 + src/Avalonia.Controls/MenuItem.cs | 7 + src/Avalonia.Controls/Primitives/Popup.cs | 14 +- src/Avalonia.Controls/Primitives/PopupRoot.cs | 7 + .../Primitives/ToggleButton.cs | 7 + src/Avalonia.Controls/ScrollViewer.cs | 7 + src/Avalonia.Controls/Slider.cs | 7 + src/Avalonia.Controls/TabControl.cs | 7 + src/Avalonia.Controls/TabItem.cs | 7 + src/Avalonia.Controls/TextBlock.cs | 9 +- src/Avalonia.Controls/TextBox.cs | 7 + src/Avalonia.Controls/Window.cs | 7 + src/Avalonia.Input/KeyboardDevice.cs | 32 +- .../AutomationNode.ExpandCollapse.cs | 19 + .../Automation/AutomationNode.RangeValue.cs | 19 + .../Automation/AutomationNode.Scroll.cs | 32 ++ .../Automation/AutomationNode.Selection.cs | 38 ++ .../Automation/AutomationNode.Toggle.cs | 13 + .../Automation/AutomationNode.Value.cs | 18 + .../Automation/AutomationNode.cs | 328 +++++++++++ .../Automation/AutomationNodeFactory.cs | 20 + .../Automation/RootAutomationNode.cs | 72 +++ .../Avalonia.Win32/Avalonia.Win32.csproj | 3 + .../Interop/Automation/IDockProvider.cs | 26 + .../Automation/IExpandCollapseProvider.cs | 16 + .../Interop/Automation/IGridItemProvider.cs | 17 + .../Interop/Automation/IGridProvider.cs | 15 + .../Interop/Automation/IInvokeProvider.cs | 19 + .../Automation/IMultipleViewProvider.cs | 16 + .../Interop/Automation/IRangeValueProvider.cs | 19 + .../IRawElementProviderAdviseEvents.cs | 15 + .../Automation/IRawElementProviderFragment.cs | 34 ++ .../IRawElementProviderFragmentRoot.cs | 14 + .../Automation/IRawElementProviderSimple.cs | 285 +++++++++ .../Automation/IRawElementProviderSimple2.cs | 14 + .../Interop/Automation/IScrollItemProvider.cs | 13 + .../Interop/Automation/IScrollProvider.cs | 21 + .../Automation/ISelectionItemProvider.cs | 17 + .../Interop/Automation/ISelectionProvider.cs | 15 + .../Automation/ISynchronizedInputProvider.cs | 26 + .../Interop/Automation/ITableItemProvider.cs | 14 + .../Interop/Automation/ITableProvider.cs | 24 + .../Interop/Automation/ITextProvider.cs | 30 + .../Interop/Automation/ITextRangeProvider.cs | 48 ++ .../Interop/Automation/IToggleProvider.cs | 15 + .../Interop/Automation/ITransformProvider.cs | 18 + .../Interop/Automation/IValueProvider.cs | 17 + .../Interop/Automation/IWindowProvider.cs | 42 ++ .../Interop/Automation/UiaCoreProviderApi.cs | 91 +++ .../Interop/Automation/UiaCoreTypesApi.cs | 62 ++ .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 16 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 28 +- .../Automation/ControlAutomationPeerTests.cs | 253 ++++++++ .../Primitives/PopupTests.cs | 1 + .../KeyboardDeviceTests.cs | 25 + .../MockWindowingPlatform.cs | 7 +- 111 files changed, 4649 insertions(+), 33 deletions(-) create mode 100644 src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs create mode 100644 src/Avalonia.Controls/Automation/AutomationLiveSetting.cs create mode 100644 src/Avalonia.Controls/Automation/AutomationProperties.cs create mode 100644 src/Avalonia.Controls/Automation/AutomationProperty.cs create mode 100644 src/Avalonia.Controls/Automation/ElementNotEnabledException.cs create mode 100644 src/Avalonia.Controls/Automation/ExpandCollapsePatternIdentifiers.cs create mode 100644 src/Avalonia.Controls/Automation/ExpandCollapseState.cs create mode 100644 src/Avalonia.Controls/Automation/IsOffscreenBehavior.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/CheckBoxAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/MenuAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/TabControlAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/TabItemAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/TextAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs create mode 100644 src/Avalonia.Controls/Automation/Platform/IAutomationNode.cs create mode 100644 src/Avalonia.Controls/Automation/Platform/IAutomationNodeFactory.cs create mode 100644 src/Avalonia.Controls/Automation/Platform/IRootAutomationNode.cs create mode 100644 src/Avalonia.Controls/Automation/Provider/IExpandCollapseProvider.cs create mode 100644 src/Avalonia.Controls/Automation/Provider/IInvokeProvider.cs create mode 100644 src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs create mode 100644 src/Avalonia.Controls/Automation/Provider/IRootProvider.cs create mode 100644 src/Avalonia.Controls/Automation/Provider/IScrollProvider.cs create mode 100644 src/Avalonia.Controls/Automation/Provider/ISelectionItemProvider .cs create mode 100644 src/Avalonia.Controls/Automation/Provider/ISelectionProvider.cs create mode 100644 src/Avalonia.Controls/Automation/Provider/IToggleProvider.cs create mode 100644 src/Avalonia.Controls/Automation/Provider/IValueProvider.cs create mode 100644 src/Avalonia.Controls/Automation/RangeValuePatternIdentifiers.cs create mode 100644 src/Avalonia.Controls/Automation/ScrollPatternIdentifiers.cs create mode 100644 src/Avalonia.Controls/Automation/SelectionPatternIdentifiers.cs create mode 100644 src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs create mode 100644 src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs create mode 100644 src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs create mode 100644 src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs create mode 100644 src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs create mode 100644 src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs create mode 100644 src/Windows/Avalonia.Win32/Automation/AutomationNode.cs create mode 100644 src/Windows/Avalonia.Win32/Automation/AutomationNodeFactory.cs create mode 100644 src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/IDockProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/IExpandCollapseProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/IGridItemProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/IGridProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/IInvokeProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/IMultipleViewProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/IRangeValueProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderAdviseEvents.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragment.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragmentRoot.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple2.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/IScrollItemProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/IScrollProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/ISelectionItemProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/ISelectionProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/ISynchronizedInputProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/ITableItemProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/ITableProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/ITextProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/ITextRangeProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/IToggleProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/ITransformProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/IValueProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/IWindowProvider.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreProviderApi.cs create mode 100644 src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs create mode 100644 tests/Avalonia.Controls.UnitTests/Automation/ControlAutomationPeerTests.cs diff --git a/src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs b/src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs new file mode 100644 index 00000000000..7f6bff11ad3 --- /dev/null +++ b/src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs @@ -0,0 +1,28 @@ +using Avalonia.Automation.Peers; + +namespace Avalonia.Automation +{ + /// + /// Contains values used as automation property identifiers by UI Automation providers. + /// + public static class AutomationElementIdentifiers + { + /// + /// Identifies the bounding rectangle automation property. The bounding rectangle property + /// value is returned by the method. + /// + public static AutomationProperty BoundingRectangleProperty { get; } = new(); + + /// + /// Identifies the class name automation property. The class name property value is returned + /// by the method. + /// + public static AutomationProperty ClassNameProperty { get; } = new(); + + /// + /// Identifies the name automation property. The class name property value is returned + /// by the method. + /// + public static AutomationProperty NameProperty { get; } = new(); + } +} diff --git a/src/Avalonia.Controls/Automation/AutomationLiveSetting.cs b/src/Avalonia.Controls/Automation/AutomationLiveSetting.cs new file mode 100644 index 00000000000..55de657b32b --- /dev/null +++ b/src/Avalonia.Controls/Automation/AutomationLiveSetting.cs @@ -0,0 +1,28 @@ +namespace Avalonia.Automation +{ + /// + /// Describes the notification characteristics of a particular live region + /// + public enum AutomationLiveSetting + { + /// + /// The element does not send notifications if the content of the live region has changed. + /// + Off = 0, + + /// + /// The element sends non-interruptive notifications if the content of the live region has + /// changed. With this setting, UI Automation clients and assistive technologies are expected + /// to not interrupt the user to inform of changes to the live region. + /// + Polite = 1, + + /// + /// The element sends interruptive notifications if the content of the live region has changed. + /// With this setting, UI Automation clients and assistive technologies are expected to interrupt + /// the user to inform of changes to the live region. + /// + Assertive = 2, + } +} + diff --git a/src/Avalonia.Controls/Automation/AutomationProperties.cs b/src/Avalonia.Controls/Automation/AutomationProperties.cs new file mode 100644 index 00000000000..8ff1210a764 --- /dev/null +++ b/src/Avalonia.Controls/Automation/AutomationProperties.cs @@ -0,0 +1,539 @@ +using System; +using Avalonia.Controls; + +namespace Avalonia.Automation +{ + public static class AutomationProperties + { + internal const int AutomationPositionInSetDefault = -1; + internal const int AutomationSizeOfSetDefault = -1; + + /// + /// Defines the AutomationProperties.AcceleratorKey attached property. + /// + public static readonly AttachedProperty AcceleratorKeyProperty = + AvaloniaProperty.RegisterAttached( + "AcceleratorKey", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.AccessKey attached property + /// + public static readonly AttachedProperty AccessKeyProperty = + AvaloniaProperty.RegisterAttached( + "AccessKey", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.AutomationId attached property. + /// + public static readonly AttachedProperty AutomationIdProperty = + AvaloniaProperty.RegisterAttached( + "AutomationId", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.HelpText attached property. + /// + public static readonly AttachedProperty HelpTextProperty = + AvaloniaProperty.RegisterAttached( + "HelpText", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.IsColumnHeader attached property. + /// + public static readonly AttachedProperty IsColumnHeaderProperty = + AvaloniaProperty.RegisterAttached( + "IsColumnHeader", + typeof(AutomationProperties), + false); + + /// + /// Defines the AutomationProperties.IsRequiredForForm attached property. + /// + public static readonly AttachedProperty IsRequiredForFormProperty = + AvaloniaProperty.RegisterAttached( + "IsRequiredForForm", + typeof(AutomationProperties), + false); + + /// + /// Defines the AutomationProperties.IsRowHeader attached property. + /// + public static readonly AttachedProperty IsRowHeaderProperty = + AvaloniaProperty.RegisterAttached( + "IsRowHeader", + typeof(AutomationProperties), + false); + + /// + /// Defines the AutomationProperties.IsOffscreenBehavior attached property. + /// + public static readonly AttachedProperty IsOffscreenBehaviorProperty = + AvaloniaProperty.RegisterAttached( + "IsOffscreenBehavior", + typeof(AutomationProperties), + IsOffscreenBehavior.Default); + + /// + /// Defines the AutomationProperties.ItemStatus attached property. + /// + public static readonly AttachedProperty ItemStatusProperty = + AvaloniaProperty.RegisterAttached( + "ItemStatus", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.ItemType attached property. + /// + public static readonly AttachedProperty ItemTypeProperty = + AvaloniaProperty.RegisterAttached( + "ItemType", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.LabeledBy attached property. + /// + public static readonly AttachedProperty LabeledByProperty = + AvaloniaProperty.RegisterAttached( + "LabeledBy", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.LiveSetting attached property. + /// + public static readonly AttachedProperty LiveSettingProperty = + AvaloniaProperty.RegisterAttached( + "LiveSetting", + typeof(AutomationProperties), + AutomationLiveSetting.Off); + + /// + /// Defines the AutomationProperties.Name attached attached property. + /// + public static readonly AttachedProperty NameProperty = + AvaloniaProperty.RegisterAttached( + "Name", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.PositionInSet attached property. + /// + /// + /// The PositionInSet property describes the ordinal location of the element within a set + /// of elements which are considered to be siblings. PositionInSet works in coordination + /// with the SizeOfSet property to describe the ordinal location in the set. + /// + public static readonly AttachedProperty PositionInSetProperty = + AvaloniaProperty.RegisterAttached( + "PositionInSet", + typeof(AutomationProperties), + AutomationPositionInSetDefault); + + /// + /// Defines the AutomationProperties.SizeOfSet attached property. + /// + /// + /// The SizeOfSet property describes the count of automation elements in a group or set + /// that are considered to be siblings. SizeOfSet works in coordination with the PositionInSet + /// property to describe the count of items in the set. + /// + public static readonly AttachedProperty SizeOfSetProperty = + AvaloniaProperty.RegisterAttached( + "SizeOfSet", + typeof(AutomationProperties), + AutomationSizeOfSetDefault); + + /// + /// Helper for setting AcceleratorKey property on a StyledElement. + /// + public static void SetAcceleratorKey(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(AcceleratorKeyProperty, value); + } + + /// + /// Helper for reading AcceleratorKey property from a StyledElement. + /// + public static string GetAcceleratorKey(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((string)element.GetValue(AcceleratorKeyProperty)); + } + + /// + /// Helper for setting AccessKey property on a StyledElement. + /// + public static void SetAccessKey(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(AccessKeyProperty, value); + } + + /// + /// Helper for reading AccessKey property from a StyledElement. + /// + public static string GetAccessKey(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((string)element.GetValue(AccessKeyProperty)); + } + + /// + /// Helper for setting AutomationId property on a StyledElement. + /// + public static void SetAutomationId(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(AutomationIdProperty, value); + } + + /// + /// Helper for reading AutomationId property from a StyledElement. + /// + public static string GetAutomationId(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return element.GetValue(AutomationIdProperty); + } + + /// + /// Helper for setting HelpText property on a StyledElement. + /// + public static void SetHelpText(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(HelpTextProperty, value); + } + + /// + /// Helper for reading HelpText property from a StyledElement. + /// + public static string GetHelpText(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((string)element.GetValue(HelpTextProperty)); + } + + /// + /// Helper for setting IsColumnHeader property on a StyledElement. + /// + public static void SetIsColumnHeader(StyledElement element, bool value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(IsColumnHeaderProperty, value); + } + + /// + /// Helper for reading IsColumnHeader property from a StyledElement. + /// + public static bool GetIsColumnHeader(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((bool)element.GetValue(IsColumnHeaderProperty)); + } + + /// + /// Helper for setting IsRequiredForForm property on a StyledElement. + /// + public static void SetIsRequiredForForm(StyledElement element, bool value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(IsRequiredForFormProperty, value); + } + + /// + /// Helper for reading IsRequiredForForm property from a StyledElement. + /// + public static bool GetIsRequiredForForm(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((bool)element.GetValue(IsRequiredForFormProperty)); + } + + /// + /// Helper for reading IsRowHeader property from a StyledElement. + /// + public static bool GetIsRowHeader(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((bool)element.GetValue(IsRowHeaderProperty)); + } + + /// + /// Helper for setting IsRowHeader property on a StyledElement. + /// + public static void SetIsRowHeader(StyledElement element, bool value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(IsRowHeaderProperty, value); + } + + /// + /// Helper for setting IsOffscreenBehavior property on a StyledElement. + /// + public static void SetIsOffscreenBehavior(StyledElement element, IsOffscreenBehavior value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(IsOffscreenBehaviorProperty, value); + } + + /// + /// Helper for reading IsOffscreenBehavior property from a StyledElement. + /// + public static IsOffscreenBehavior GetIsOffscreenBehavior(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((IsOffscreenBehavior)element.GetValue(IsOffscreenBehaviorProperty)); + } + + /// + /// Helper for setting ItemStatus property on a StyledElement. + /// + public static void SetItemStatus(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(ItemStatusProperty, value); + } + + /// + /// Helper for reading ItemStatus property from a StyledElement. + /// + public static string GetItemStatus(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((string)element.GetValue(ItemStatusProperty)); + } + + /// + /// Helper for setting ItemType property on a StyledElement. + /// + public static void SetItemType(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(ItemTypeProperty, value); + } + + /// + /// Helper for reading ItemType property from a StyledElement. + /// + public static string GetItemType(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((string)element.GetValue(ItemTypeProperty)); + } + + /// + /// Helper for setting LabeledBy property on a StyledElement. + /// + public static void SetLabeledBy(StyledElement element, IControl value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(LabeledByProperty, value); + } + + /// + /// Helper for reading LabeledBy property from a StyledElement. + /// + public static IControl GetLabeledBy(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return element.GetValue(LabeledByProperty); + } + + /// + /// Helper for setting LiveSetting property on a StyledElement. + /// + public static void SetLiveSetting(StyledElement element, AutomationLiveSetting value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(LiveSettingProperty, value); + } + + /// + /// Helper for reading LiveSetting property from a StyledElement. + /// + public static AutomationLiveSetting GetLiveSetting(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((AutomationLiveSetting)element.GetValue(LiveSettingProperty)); + } + + /// + /// Helper for setting Name property on a StyledElement. + /// + public static void SetName(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(NameProperty, value); + } + + /// + /// Helper for reading Name property from a StyledElement. + /// + public static string GetName(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((string)element.GetValue(NameProperty)); + } + + /// + /// Helper for setting PositionInSet property on a StyledElement. + /// + public static void SetPositionInSet(StyledElement element, int value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(PositionInSetProperty, value); + } + + /// + /// Helper for reading PositionInSet property from a StyledElement. + /// + public static int GetPositionInSet(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((int)element.GetValue(PositionInSetProperty)); + } + + /// + /// Helper for setting SizeOfSet property on a StyledElement. + /// + public static void SetSizeOfSet(StyledElement element, int value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(SizeOfSetProperty, value); + } + + /// + /// Helper for reading SizeOfSet property from a StyledElement. + /// + public static int GetSizeOfSet(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((int)element.GetValue(SizeOfSetProperty)); + } + } +} + diff --git a/src/Avalonia.Controls/Automation/AutomationProperty.cs b/src/Avalonia.Controls/Automation/AutomationProperty.cs new file mode 100644 index 00000000000..16968b271df --- /dev/null +++ b/src/Avalonia.Controls/Automation/AutomationProperty.cs @@ -0,0 +1,11 @@ +namespace Avalonia.Automation +{ + /// + /// Identifies a property of or of a specific + /// control pattern. + /// + public sealed class AutomationProperty + { + internal AutomationProperty() { } + } +} diff --git a/src/Avalonia.Controls/Automation/ElementNotEnabledException.cs b/src/Avalonia.Controls/Automation/ElementNotEnabledException.cs new file mode 100644 index 00000000000..ac73d50603e --- /dev/null +++ b/src/Avalonia.Controls/Automation/ElementNotEnabledException.cs @@ -0,0 +1,10 @@ +using System; + +namespace Avalonia.Automation +{ + public class ElementNotEnabledException : Exception + { + public ElementNotEnabledException() : base("Element not enabled.") { } + public ElementNotEnabledException(string message) : base(message) { } + } +} diff --git a/src/Avalonia.Controls/Automation/ExpandCollapsePatternIdentifiers.cs b/src/Avalonia.Controls/Automation/ExpandCollapsePatternIdentifiers.cs new file mode 100644 index 00000000000..342bdaceb12 --- /dev/null +++ b/src/Avalonia.Controls/Automation/ExpandCollapsePatternIdentifiers.cs @@ -0,0 +1,15 @@ +using Avalonia.Automation.Provider; + +namespace Avalonia.Automation +{ + /// + /// Contains values used as identifiers by . + /// + public static class ExpandCollapsePatternIdentifiers + { + /// + /// Identifies automation property. + /// + public static AutomationProperty ExpandCollapseStateProperty { get; } = new(); + } +} diff --git a/src/Avalonia.Controls/Automation/ExpandCollapseState.cs b/src/Avalonia.Controls/Automation/ExpandCollapseState.cs new file mode 100644 index 00000000000..c6b4feeb505 --- /dev/null +++ b/src/Avalonia.Controls/Automation/ExpandCollapseState.cs @@ -0,0 +1,29 @@ +namespace Avalonia.Automation +{ + /// + /// Contains values that specify the of a UI Automation element. + /// + public enum ExpandCollapseState + { + /// + /// No child nodes, controls, or content of the UI Automation element are displayed. + /// + Collapsed, + + /// + /// All child nodes, controls or content of the UI Automation element are displayed. + /// + Expanded, + + /// + /// The UI Automation element has no child nodes, controls, or content to display. + /// + LeafNode, + + /// + /// Some, but not all, child nodes, controls, or content of the UI Automation element are + /// displayed. + /// + PartiallyExpanded + } +} diff --git a/src/Avalonia.Controls/Automation/IsOffscreenBehavior.cs b/src/Avalonia.Controls/Automation/IsOffscreenBehavior.cs new file mode 100644 index 00000000000..128c1e1dcc6 --- /dev/null +++ b/src/Avalonia.Controls/Automation/IsOffscreenBehavior.cs @@ -0,0 +1,26 @@ +namespace Avalonia.Automation +{ + /// + /// This enum offers different ways of evaluating the IsOffscreen AutomationProperty + /// + public enum IsOffscreenBehavior + { + /// + /// The AutomationProperty IsOffscreen is calculated based on IsVisible. + /// + Default, + /// + /// The AutomationProperty IsOffscreen is false. + /// + Onscreen, + /// + /// The AutomationProperty IsOffscreen if true. + /// + Offscreen, + /// + /// The AutomationProperty IsOffscreen is calculated based on clip regions. + /// + FromClip, + } +} + diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs new file mode 100644 index 00000000000..aefd1febd06 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -0,0 +1,227 @@ +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Automation.Platform; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public enum AutomationControlType + { + Button, + Calendar, + CheckBox, + ComboBox, + Edit, + Hyperlink, + Image, + ListItem, + List, + Menu, + MenuBar, + MenuItem, + ProgressBar, + RadioButton, + ScrollBar, + Slider, + Spinner, + StatusBar, + Tab, + TabItem, + Text, + ToolBar, + ToolTip, + Tree, + TreeItem, + Custom, + Group, + Thumb, + DataGrid, + DataItem, + Document, + SplitButton, + Window, + Pane, + Header, + HeaderItem, + Table, + TitleBar, + Separator, + } + + /// + /// Provides a base class that exposes an element to UI Automation. + /// + public abstract class AutomationPeer + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The factory to use to create the platform automation node. + /// + protected AutomationPeer(IAutomationNodeFactory factory) + { + Node = factory.CreateNode(this); + } + + /// + /// Gets the related node in the platform UI Automation tree. + /// + public IAutomationNode Node { get; } + + /// + /// Attempts to bring the element associated with the automation peer into view. + /// + public void BringIntoView() => BringIntoViewCore(); + + /// + /// Gets the control type for the element that is associated with the UI Automation peer. + /// + public AutomationControlType GetAutomationControlType() => GetAutomationControlTypeCore(); + + /// + /// Gets the automation ID of the element that is associated with the UI Automation peer. + /// + public string? GetAutomationId() => GetAutomationIdCore(); + + /// + /// Gets the bounding rectangle of the element that is associated with the automation peer + /// in top-level coordinates. + /// + public Rect GetBoundingRectangle() => GetBoundingRectangleCore(); + + /// + /// Gets the child automation peers. + /// + public IReadOnlyList GetChildren() => GetOrCreateChildrenCore(); + + /// + /// Gets a string that describes the class of the element. + /// + public string GetClassName() => GetClassNameCore() ?? string.Empty; + + /// + /// Gets a human-readable localized string that represents the type of the control that is + /// associated with this automation peer. + /// + public string GetLocalizedControlType() => GetLocalizedControlTypeCore(); + + /// + /// Gets text that describes the element that is associated with this automation peer. + /// + public string GetName() => GetNameCore() ?? string.Empty; + + /// + /// Gets the that is the parent of this . + /// + /// + public AutomationPeer? GetParent() => GetParentCore(); + + /// + /// Gets a value that indicates whether the element that is associated with this automation + /// peer currently has keyboard focus. + /// + public bool HasKeyboardFocus() => HasKeyboardFocusCore(); + + /// + /// Gets a value that indicates whether the element that is associated with this automation + /// peer contains data that is presented to the user. + /// + public bool IsContentElement() => IsControlElement() && IsContentElementCore(); + + /// + /// Gets a value that indicates whether the element is understood by the user as + /// interactive or as contributing to the logical structure of the control in the GUI. + /// + public bool IsControlElement() => IsControlElementCore(); + + /// + /// Gets a value indicating whether the control is enabled for user interaction. + /// + public bool IsEnabled() => IsEnabledCore(); + + /// + /// Gets a value that indicates whether the element can accept keyboard focus. + /// + /// + public bool IsKeyboardFocusable() => IsKeyboardFocusableCore(); + + /// + /// Sets the keyboard focus on the element that is associated with this automation peer. + /// + public void SetFocus() => SetFocusCore(); + + /// + /// Shows the context menu for the element that is associated with this automation peer. + /// + /// true if a context menu is present for the element; otherwise false. + public bool ShowContextMenu() => ShowContextMenuCore(); + + /// + /// Raises an event to notify the automation client of a changed property value. + /// + /// The property that changed. + /// The previous value of the property. + /// The new value of the property. + public void RaisePropertyChangedEvent( + AutomationProperty automationProperty, + object? oldValue, + object? newValue) + { + Node.PropertyChanged(automationProperty, oldValue, newValue); + } + + protected virtual string GetLocalizedControlTypeCore() + { + var controlType = GetAutomationControlType(); + + return controlType switch + { + AutomationControlType.CheckBox => "check box", + AutomationControlType.ComboBox => "combo box", + AutomationControlType.ListItem => "list item", + AutomationControlType.MenuBar => "menu bar", + AutomationControlType.MenuItem => "menu item", + AutomationControlType.ProgressBar => "progress bar", + AutomationControlType.RadioButton => "radio button", + AutomationControlType.ScrollBar => "scroll bar", + AutomationControlType.StatusBar => "status bar", + AutomationControlType.TabItem => "tab item", + AutomationControlType.ToolBar => "toolbar", + AutomationControlType.ToolTip => "tooltip", + AutomationControlType.TreeItem => "tree item", + AutomationControlType.Custom => "custom", + AutomationControlType.DataGrid => "data grid", + AutomationControlType.DataItem => "data item", + AutomationControlType.SplitButton => "split button", + AutomationControlType.HeaderItem => "header item", + AutomationControlType.TitleBar => "title bar", + _ => controlType.ToString().ToLowerInvariant(), + }; + } + + protected abstract void BringIntoViewCore(); + protected abstract AutomationControlType GetAutomationControlTypeCore(); + protected abstract string? GetAutomationIdCore(); + protected abstract Rect GetBoundingRectangleCore(); + protected abstract IReadOnlyList GetOrCreateChildrenCore(); + protected abstract string GetClassNameCore(); + protected abstract string? GetNameCore(); + protected abstract AutomationPeer? GetParentCore(); + protected abstract bool HasKeyboardFocusCore(); + protected abstract bool IsContentElementCore(); + protected abstract bool IsControlElementCore(); + protected abstract bool IsEnabledCore(); + protected abstract bool IsKeyboardFocusableCore(); + protected abstract void SetFocusCore(); + protected abstract bool ShowContextMenuCore(); + protected internal abstract bool TrySetParent(AutomationPeer? parent); + + protected void EnsureEnabled() + { + if (!IsEnabled()) + throw new ElementNotEnabledException(); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs new file mode 100644 index 00000000000..ded27f14bdc --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs @@ -0,0 +1,29 @@ +using Avalonia.Automation.Platform; +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class ButtonAutomationPeer : ContentControlAutomationPeer, + IInvokeProvider + { + public ButtonAutomationPeer(IAutomationNodeFactory factory, Button owner) + : base(factory, owner) + { + } + + public void Invoke() + { + EnsureEnabled(); + (Owner as Button)?.PerformClick(); + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Button; + } + } +} + diff --git a/src/Avalonia.Controls/Automation/Peers/CheckBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/CheckBoxAutomationPeer.cs new file mode 100644 index 00000000000..4e98cc7746b --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/CheckBoxAutomationPeer.cs @@ -0,0 +1,20 @@ +using Avalonia.Automation.Platform; +using Avalonia.Controls; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class CheckBoxAutomationPeer : ToggleButtonAutomationPeer + { + public CheckBoxAutomationPeer(IAutomationNodeFactory factory, CheckBox owner) + : base(factory, owner) + { + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.CheckBox; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs new file mode 100644 index 00000000000..698bfd14607 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs @@ -0,0 +1,123 @@ +using System.Collections.Generic; +using Avalonia.Automation.Platform; +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class ComboBoxAutomationPeer : SelectingItemsControlAutomationPeer, + IExpandCollapseProvider + { + private UnrealizedSelectionPeer[]? _selection; + + public ComboBoxAutomationPeer(IAutomationNodeFactory factory, ComboBox owner) + : base(factory, owner) + { + } + + public new ComboBox Owner => (ComboBox)base.Owner; + + public ExpandCollapseState ExpandCollapseState => ToState(Owner.IsDropDownOpen); + public void Collapse() => Owner.IsDropDownOpen = false; + public void Expand() => Owner.IsDropDownOpen = true; + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.ComboBox; + } + + protected override IReadOnlyList? GetSelectionCore() + { + if (ExpandCollapseState == ExpandCollapseState.Expanded) + return base.GetSelectionCore(); + + // If the combo box is not open then we won't have an ItemsPresenter so the default + // GetSelectionCore implementation won't work. For this case we create a separate + // peer to represent the unrealized item. + if (Owner.SelectedItem is object selection) + { + _selection ??= new[] { new UnrealizedSelectionPeer(Node.Factory, this) }; + _selection[0].Item = selection; + return _selection; + } + + return null; + } + + protected override void OwnerPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + base.OwnerPropertyChanged(sender, e); + + if (e.Property == ComboBox.IsDropDownOpenProperty) + { + RaisePropertyChangedEvent( + ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, + ToState((bool)e.OldValue!), + ToState((bool)e.NewValue!)); + } + } + + private ExpandCollapseState ToState(bool value) + { + return value ? ExpandCollapseState.Expanded : ExpandCollapseState.Collapsed; + } + + private class UnrealizedSelectionPeer : UnrealizedElementAutomationPeer + { + private readonly ComboBoxAutomationPeer _owner; + private object? _item; + + public UnrealizedSelectionPeer(IAutomationNodeFactory factory, ComboBoxAutomationPeer owner) + : base(factory) + { + _owner = owner; + } + + public object? Item + { + get => _item; + set + { + if (_item != value) + { + var oldValue = GetNameCore(); + _item = value; + RaisePropertyChangedEvent( + AutomationElementIdentifiers.NameProperty, + oldValue, + GetNameCore()); + } + } + } + + protected override string? GetAutomationIdCore() => null; + protected override string GetClassNameCore() => typeof(ComboBoxItem).Name; + protected override AutomationPeer? GetParentCore() => _owner; + protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.ListItem; + + protected override string? GetNameCore() + { + if (_item is Control c) + { + var result = AutomationProperties.GetName(c); + + if (result is null && c is ContentControl cc && cc.Presenter?.Child is TextBlock text) + { + result = text.Text; + } + + if (result is null) + { + result = c.GetValue(ContentControl.ContentProperty)?.ToString(); + } + + return result; + } + + return _item?.ToString(); + } + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs new file mode 100644 index 00000000000..db1c7e1aa7b --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs @@ -0,0 +1,39 @@ +using Avalonia.Automation.Platform; +using Avalonia.Controls; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class ContentControlAutomationPeer : ControlAutomationPeer + { + protected ContentControlAutomationPeer(IAutomationNodeFactory factory, ContentControl owner) + : base(factory, owner) + { + } + + public new ContentControl Owner => (ContentControl)base.Owner; + + protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Pane; + + protected override string? GetNameCore() + { + var result = base.GetNameCore(); + + if (result is null && Owner.Presenter?.Child is TextBlock text) + { + result = text.Text; + } + + if (result is null) + { + result = Owner.Content?.ToString(); + } + + return result; + } + + protected override bool IsContentElementCore() => false; + protected override bool IsControlElementCore() => false; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs new file mode 100644 index 00000000000..79899866530 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Automation.Platform; +using Avalonia.Controls; +using Avalonia.VisualTree; + +#nullable enable + +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(IAutomationNodeFactory factory, Control owner) + : base(factory) + { + Owner = owner ?? throw new ArgumentNullException("owner"); + + owner.PropertyChanged += OwnerPropertyChanged; + var visualChildren = ((IVisual)owner).VisualChildren; + visualChildren.CollectionChanged += VisualChildrenChanged; + } + + public Control Owner { get; } + + public static AutomationPeer GetOrCreatePeer(IAutomationNodeFactory factory, Control element) + { + element = element ?? throw new ArgumentNullException("element"); + return element.GetOrCreateAutomationPeer(factory); + } + + public AutomationPeer GetOrCreatePeer(Control element) + { + return element == Owner ? this : GetOrCreatePeer(Node.Factory, element); + } + + protected override void BringIntoViewCore() => Owner.BringIntoView(); + protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner); + protected override Rect GetBoundingRectangleCore() => GetBounds(Owner.TransformedBounds); + + protected virtual IReadOnlyList? GetChildrenCore() + { + var children = ((IVisual)Owner).VisualChildren; + + if (children.Count == 0) + return null; + + var result = new List(); + + foreach (var child in children) + { + if (child is Control c && c.IsVisible) + { + result.Add(GetOrCreatePeer(c)); + } + } + + return result; + } + + protected override AutomationPeer? GetParentCore() + { + EnsureConnected(); + return _parent; + } + + /// + /// Invalidates the peer's children and causes a re-read from . + /// + protected void InvalidateChildren() + { + _childrenValid = false; + Node!.ChildrenChanged(); + } + + /// + /// Invalidates the peer's parent. + /// + protected void InvalidateParent() + { + _parent = null; + _parentValid = false; + } + + 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 override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Custom; + protected override string GetClassNameCore() => Owner.GetType().Name; + protected override string? GetNameCore() => AutomationProperties.GetName(Owner); + protected override bool HasKeyboardFocusCore() => Owner.IsFocused; + protected override bool IsContentElementCore() => true; + protected override bool IsControlElementCore() => true; + protected override bool IsEnabledCore() => Owner.IsEnabled; + protected override bool IsKeyboardFocusableCore() => Owner.Focusable; + protected override void SetFocusCore() => Owner.Focus(); + + 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; + } + + private Rect GetBounds(TransformedBounds? bounds) + { + return bounds?.Bounds.TransformToAABB(bounds!.Value.Transform) ?? default; + } + + 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) + (GetOrCreatePeer(c) as ControlAutomationPeer)?.InvalidateChildren(); + } + else if (e.Property == Visual.TransformedBoundsProperty) + { + RaisePropertyChangedEvent( + AutomationElementIdentifiers.BoundingRectangleProperty, + GetBounds((TransformedBounds?)e.OldValue), + GetBounds((TransformedBounds?)e.NewValue)); + } + 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 = GetOrCreatePeer(c); + parentPeer.GetChildren(); + } + + parent = parent.GetVisualParent(); + } + + _parentValid = true; + } + } + } +} + diff --git a/src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs new file mode 100644 index 00000000000..ad88941299e --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs @@ -0,0 +1,20 @@ +using Avalonia.Automation.Platform; +using Avalonia.Controls; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class ImageAutomationPeer : ControlAutomationPeer + { + public ImageAutomationPeer(IAutomationNodeFactory factory, Control owner) + : base(factory, owner) + { + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Image; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs new file mode 100644 index 00000000000..6eed22e6cff --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs @@ -0,0 +1,57 @@ +using Avalonia.Automation.Platform; +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class ItemsControlAutomationPeer : ControlAutomationPeer, IScrollProvider + { + private bool _searchedForScrollable; + private IScrollProvider? _scroller; + + public ItemsControlAutomationPeer(IAutomationNodeFactory factory, ItemsControl owner) + : base(factory, owner) + { + } + + public new ItemsControl Owner => (ItemsControl)base.Owner; + public bool HorizontallyScrollable => _scroller?.HorizontallyScrollable ?? false; + public double HorizontalScrollPercent => _scroller?.HorizontalScrollPercent ?? -1; + public double HorizontalViewSize => _scroller?.HorizontalViewSize ?? 0; + public bool VerticallyScrollable => _scroller?.VerticallyScrollable ?? false; + public double VerticalScrollPercent => _scroller?.VerticalScrollPercent ?? -1; + public double VerticalViewSize => _scroller?.VerticalViewSize ?? 0; + + protected virtual IScrollProvider? Scroller + { + get + { + if (!_searchedForScrollable) + { + if (Owner.GetValue(ListBox.ScrollProperty) is Control scrollable) + _scroller = GetOrCreatePeer(scrollable) as IScrollProvider; + _searchedForScrollable = true; + } + + return _scroller; + } + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.List; + } + + public void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount) + { + _scroller?.Scroll(horizontalAmount, verticalAmount); + } + + public void SetScrollPercent(double horizontalPercent, double verticalPercent) + { + _scroller?.SetScrollPercent(horizontalPercent, verticalPercent); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs new file mode 100644 index 00000000000..a9e5089e434 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs @@ -0,0 +1,82 @@ +using System; +using Avalonia.Automation.Platform; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Selection; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class ListItemAutomationPeer : ContentControlAutomationPeer, + ISelectionItemProvider + { + public ListItemAutomationPeer(IAutomationNodeFactory factory, ContentControl owner) + : base(factory, owner) + { + } + + public bool IsSelected => Owner.GetValue(ListBoxItem.IsSelectedProperty); + + public ISelectionProvider? SelectionContainer + { + get + { + if (Owner.Parent is Control parent) + { + var parentPeer = GetOrCreatePeer(parent); + return parentPeer as ISelectionProvider; + } + + return null; + } + } + + public void Select() + { + EnsureEnabled(); + + if (Owner.Parent is SelectingItemsControl parent) + { + var index = parent.ItemContainerGenerator.IndexFromContainer(Owner); + + if (index != -1) + parent.SelectedIndex = index; + } + } + + void ISelectionItemProvider.AddToSelection() + { + EnsureEnabled(); + + if (Owner.Parent is ItemsControl parent && + parent.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel) + { + var index = parent.ItemContainerGenerator.IndexFromContainer(Owner); + + if (index != -1) + selectionModel.Select(index); + } + } + + void ISelectionItemProvider.RemoveFromSelection() + { + EnsureEnabled(); + + if (Owner.Parent is ItemsControl parent && + parent.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel) + { + var index = parent.ItemContainerGenerator.IndexFromContainer(Owner); + + if (index != -1) + selectionModel.Deselect(index); + } + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.ListItem; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/MenuAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/MenuAutomationPeer.cs new file mode 100644 index 00000000000..38cd7a8d416 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/MenuAutomationPeer.cs @@ -0,0 +1,22 @@ +using Avalonia.Automation.Platform; +using Avalonia.Controls; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class MenuAutomationPeer : ControlAutomationPeer + { + public MenuAutomationPeer(IAutomationNodeFactory factory, Menu owner) + : base(factory, owner) + { + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Menu; + } + + protected override bool IsContentElementCore() => false; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs new file mode 100644 index 00000000000..b1f7774fc93 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs @@ -0,0 +1,39 @@ +using Avalonia.Automation.Platform; +using Avalonia.Controls; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class MenuItemAutomationPeer : ControlAutomationPeer + { + public MenuItemAutomationPeer(IAutomationNodeFactory factory, MenuItem owner) + : base(factory, owner) + { + } + + public new MenuItem Owner => (MenuItem)base.Owner; + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.MenuItem; + } + + protected override string? GetNameCore() + { + var result = base.GetNameCore(); + + if (result is null && Owner.HeaderPresenter.Child is TextBlock text) + { + result = text.Text; + } + + if (result is null) + { + result = Owner.Header?.ToString(); + } + + return result; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs new file mode 100644 index 00000000000..894234e5023 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs @@ -0,0 +1,28 @@ +using Avalonia.Automation.Platform; +using Avalonia.Controls; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + /// + /// An automation peer which represents an element that is exposed to automation as non- + /// interactive or as not contributing to the logical structure of the application. + /// + public class NoneAutomationPeer : ControlAutomationPeer + { + public NoneAutomationPeer(IAutomationNodeFactory factory, Control owner) + : base(factory, owner) + { + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Pane; + } + + protected override bool IsContentElementCore() => false; + protected override bool IsControlElementCore() => false; + } +} + diff --git a/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs new file mode 100644 index 00000000000..ed0e8f6d440 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs @@ -0,0 +1,36 @@ +using System; +using Avalonia.Automation.Platform; +using Avalonia.Controls.Primitives; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class PopupRootAutomationPeer : WindowBaseAutomationPeer + { + public PopupRootAutomationPeer(IAutomationNodeFactory factory, PopupRoot owner) + : base(factory, owner) + { + if (owner.IsVisible) + StartTrackingFocus(); + else + owner.Opened += OnOpened; + owner.Closed += OnClosed; + } + + protected override bool IsContentElementCore() => false; + protected override bool IsControlElementCore() => false; + + private void OnOpened(object sender, EventArgs e) + { + ((PopupRoot)Owner).Opened -= OnOpened; + StartTrackingFocus(); + } + + private void OnClosed(object sender, EventArgs e) + { + ((PopupRoot)Owner).Closed -= OnClosed; + StopTrackingFocus(); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs new file mode 100644 index 00000000000..bafc4c14fc9 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs @@ -0,0 +1,34 @@ +using Avalonia.Automation.Platform; +using Avalonia.Automation.Provider; +using Avalonia.Controls.Primitives; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public abstract class RangeBaseAutomationPeer : ControlAutomationPeer, IRangeValueProvider + { + public RangeBaseAutomationPeer(IAutomationNodeFactory factory, RangeBase owner) + : base(factory, owner) + { + owner.PropertyChanged += OwnerPropertyChanged; + } + + public new RangeBase Owner => (RangeBase)base.Owner; + public virtual bool IsReadOnly => false; + public double Maximum => Owner.Maximum; + public double Minimum => Owner.Minimum; + public double Value => Owner.Value; + public void SetValue(double value) => Owner.Value = value; + + protected virtual void OwnerPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == RangeBase.MinimumProperty) + RaisePropertyChangedEvent(RangeValuePatternIdentifiers.MinimumProperty, e.OldValue, e.NewValue); + else if (e.Property == RangeBase.MaximumProperty) + RaisePropertyChangedEvent(RangeValuePatternIdentifiers.MaximumProperty, e.OldValue, e.NewValue); + else if (e.Property == RangeBase.ValueProperty) + RaisePropertyChangedEvent(RangeValuePatternIdentifiers.ValueProperty, e.OldValue, e.NewValue); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs new file mode 100644 index 00000000000..165df6c0322 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs @@ -0,0 +1,175 @@ +using System; +using Avalonia.Automation.Platform; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.Utilities; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class ScrollViewerAutomationPeer : ControlAutomationPeer, IScrollProvider + { + public ScrollViewerAutomationPeer(IAutomationNodeFactory factory, ScrollViewer owner) + : base(factory, owner) + { + } + + public new ScrollViewer Owner => (ScrollViewer)base.Owner; + + public bool HorizontallyScrollable + { + get => MathUtilities.GreaterThan(Owner.Extent.Width, Owner.Viewport.Width); + } + + public double HorizontalScrollPercent + { + get + { + if (!HorizontallyScrollable) + return ScrollPatternIdentifiers.NoScroll; + return (double)(Owner.Offset.X * 100.0 / (Owner.Extent.Width - Owner.Viewport.Width)); + } + } + + public double HorizontalViewSize + { + get + { + if (MathUtilities.IsZero(Owner.Extent.Width)) + return 100; + return Math.Min(100, Owner.Viewport.Width * 100.0 / Owner.Extent.Width); + } + } + + public bool VerticallyScrollable + { + get => MathUtilities.GreaterThan(Owner.Extent.Height, Owner.Viewport.Height); + } + + public double VerticalScrollPercent + { + get + { + if (!VerticallyScrollable) + return ScrollPatternIdentifiers.NoScroll; + return (double)(Owner.Offset.Y * 100.0 / (Owner.Extent.Height - Owner.Viewport.Height)); + } + } + + public double VerticalViewSize + { + get + { + if (MathUtilities.IsZero(Owner.Extent.Height)) + return 100; + return Math.Min(100, Owner.Viewport.Height * 100.0 / Owner.Extent.Height); + } + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Pane; + } + + protected override bool IsContentElementCore() => false; + + protected override bool IsControlElementCore() + { + // Return false if the control is part of a control template. + return Owner.TemplatedParent is null && base.IsControlElementCore(); + } + + public void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount) + { + if (!IsEnabled()) + throw new ElementNotEnabledException(); + + var scrollHorizontally = horizontalAmount != ScrollAmount.NoAmount; + var scrollVertically = verticalAmount != ScrollAmount.NoAmount; + + if (scrollHorizontally && !HorizontallyScrollable || scrollVertically && !VerticallyScrollable) + { + throw new InvalidOperationException("Operation cannot be performed"); + } + + switch (horizontalAmount) + { + case ScrollAmount.LargeDecrement: + Owner.PageLeft(); + break; + case ScrollAmount.SmallDecrement: + Owner.LineLeft(); + break; + case ScrollAmount.SmallIncrement: + Owner.LineRight(); + break; + case ScrollAmount.LargeIncrement: + Owner.PageRight(); + break; + case ScrollAmount.NoAmount: + break; + default: + throw new InvalidOperationException("Operation cannot be performed"); + } + + switch (verticalAmount) + { + case ScrollAmount.LargeDecrement: + Owner.PageUp(); + break; + case ScrollAmount.SmallDecrement: + Owner.LineUp(); + break; + case ScrollAmount.SmallIncrement: + Owner.LineDown(); + break; + case ScrollAmount.LargeIncrement: + Owner.PageDown(); + break; + case ScrollAmount.NoAmount: + break; + default: + throw new InvalidOperationException("Operation cannot be performed"); + } + } + + public void SetScrollPercent(double horizontalPercent, double verticalPercent) + { + if (!IsEnabled()) + throw new ElementNotEnabledException(); + + var scrollHorizontally = horizontalPercent != ScrollPatternIdentifiers.NoScroll; + var scrollVertically = verticalPercent != ScrollPatternIdentifiers.NoScroll; + + if (scrollHorizontally && !HorizontallyScrollable || scrollVertically && !VerticallyScrollable) + { + throw new InvalidOperationException("Operation cannot be performed"); + } + + if (scrollHorizontally && (horizontalPercent < 0.0) || (horizontalPercent > 100.0)) + { + throw new ArgumentOutOfRangeException("horizontalPercent"); + } + + if (scrollVertically && (verticalPercent < 0.0) || (verticalPercent > 100.0)) + { + throw new ArgumentOutOfRangeException("verticalPercent"); + } + + var offset = Owner.Offset; + + if (scrollHorizontally) + { + offset = offset.WithX((Owner.Extent.Width - Owner.Viewport.Width) * horizontalPercent * 0.01); + } + + if (scrollVertically) + { + offset = offset.WithY((Owner.Extent.Height - Owner.Viewport.Height) * verticalPercent * 0.01); + } + + Owner.Offset = offset; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs new file mode 100644 index 00000000000..b55f653a5d9 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using Avalonia.Automation.Platform; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Selection; +using Avalonia.VisualTree; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public abstract class SelectingItemsControlAutomationPeer : ItemsControlAutomationPeer, + ISelectionProvider + { + private ISelectionModel _selection; + + protected SelectingItemsControlAutomationPeer(IAutomationNodeFactory factory, SelectingItemsControl owner) + : base(factory, owner) + { + _selection = owner.GetValue(ListBox.SelectionProperty); + _selection.SelectionChanged += OwnerSelectionChanged; + owner.PropertyChanged += OwnerPropertyChanged; + } + + public bool CanSelectMultiple => GetSelectionModeCore().HasFlagCustom(SelectionMode.Multiple); + public bool IsSelectionRequired => GetSelectionModeCore().HasFlagCustom(SelectionMode.AlwaysSelected); + public IReadOnlyList GetSelection() => GetSelectionCore() ?? Array.Empty(); + + protected virtual IReadOnlyList? GetSelectionCore() + { + List? result = null; + + if (Owner is SelectingItemsControl owner) + { + var selection = Owner.GetValue(ListBox.SelectionProperty); + + foreach (var i in selection.SelectedIndexes) + { + var container = owner.ItemContainerGenerator.ContainerFromIndex(i); + + if (container is Control c && ((IVisual)c).IsAttachedToVisualTree) + { + var peer = GetOrCreatePeer(c); + + if (peer is object) + { + result ??= new List(); + result.Add(peer); + } + } + } + + return result; + } + + return result; + } + + protected virtual SelectionMode GetSelectionModeCore() + { + return (Owner as SelectingItemsControl)?.GetValue(ListBox.SelectionModeProperty) ?? SelectionMode.Single; + } + + protected virtual void OwnerPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == ListBox.SelectionProperty) + { + _selection.SelectionChanged -= OwnerSelectionChanged; + _selection = Owner.GetValue(ListBox.SelectionProperty); + _selection.SelectionChanged += OwnerSelectionChanged; + RaiseSelectionChanged(); + } + } + + protected virtual void OwnerSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) + { + RaiseSelectionChanged(); + } + + private void RaiseSelectionChanged() + { + RaisePropertyChangedEvent(SelectionPatternIdentifiers.SelectionProperty, null, null); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs new file mode 100644 index 00000000000..172b9a1b544 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs @@ -0,0 +1,20 @@ +using Avalonia.Automation.Platform; +using Avalonia.Controls; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class SliderAutomationPeer : RangeBaseAutomationPeer + { + public SliderAutomationPeer(IAutomationNodeFactory factory, Slider owner) + : base(factory, owner) + { + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Slider; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/TabControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TabControlAutomationPeer.cs new file mode 100644 index 00000000000..d615be43f3b --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/TabControlAutomationPeer.cs @@ -0,0 +1,20 @@ +using Avalonia.Automation.Platform; +using Avalonia.Controls; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class TabControlAutomationPeer : SelectingItemsControlAutomationPeer + { + public TabControlAutomationPeer(IAutomationNodeFactory factory, TabControl owner) + : base(factory, owner) + { + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Tab; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/TabItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TabItemAutomationPeer.cs new file mode 100644 index 00000000000..20021b5b963 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/TabItemAutomationPeer.cs @@ -0,0 +1,18 @@ +using Avalonia.Automation.Platform; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class TabItemAutomationPeer : ListItemAutomationPeer + { + public TabItemAutomationPeer(IAutomationNodeFactory factory, TabItem owner) + : base(factory, owner) + { + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.TabItem; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/TextAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TextAutomationPeer.cs new file mode 100644 index 00000000000..c523feb0f8a --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/TextAutomationPeer.cs @@ -0,0 +1,28 @@ +using Avalonia.Automation.Platform; +using Avalonia.Controls; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class TextAutomationPeer : ControlAutomationPeer + { + public TextAutomationPeer(IAutomationNodeFactory factory, Control owner) + : base(factory, owner) + { + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Text; + } + + protected override string? GetNameCore() => Owner.GetValue(TextBlock.TextProperty); + + protected override bool IsControlElementCore() + { + // Return false if the control is part of a control template. + return Owner.TemplatedParent is null && base.IsControlElementCore(); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs new file mode 100644 index 00000000000..8ee1aacb524 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs @@ -0,0 +1,21 @@ +using Avalonia.Automation.Platform; +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class TextBoxAutomationPeer : TextAutomationPeer, IValueProvider + { + public TextBoxAutomationPeer(IAutomationNodeFactory factory, TextBox owner) + : base(factory, owner) + { + } + + public new TextBox Owner => (TextBox)base.Owner; + public bool IsReadOnly => Owner.IsReadOnly; + public string? Value => Owner.Text; + public void SetValue(string? value) => Owner.Text = value; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs new file mode 100644 index 00000000000..b103df31653 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs @@ -0,0 +1,39 @@ +using Avalonia.Automation.Platform; +using Avalonia.Automation.Provider; +using Avalonia.Controls.Primitives; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class ToggleButtonAutomationPeer : ContentControlAutomationPeer, IToggleProvider + { + public ToggleButtonAutomationPeer(IAutomationNodeFactory factory, ToggleButton owner) + : base(factory, owner) + { + } + + public new ToggleButton Owner => (ToggleButton)base.Owner; + + ToggleState IToggleProvider.ToggleState + { + get => Owner.IsChecked switch + { + true => ToggleState.On, + false => ToggleState.Off, + null => ToggleState.Indeterminate, + }; + } + + void IToggleProvider.Toggle() + { + EnsureEnabled(); + Owner.PerformClick(); + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Button; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs new file mode 100644 index 00000000000..5832b04dd7b --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using Avalonia.Automation.Platform; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + /// + /// An automation peer which represents an unrealized element + /// + public abstract class UnrealizedElementAutomationPeer : AutomationPeer + { + protected UnrealizedElementAutomationPeer(IAutomationNodeFactory factory) + : base(factory) + { + } + + public void SetParent(AutomationPeer? parent) => TrySetParent(parent); + protected override void BringIntoViewCore() => GetParent()?.BringIntoView(); + protected override Rect GetBoundingRectangleCore() => GetParent()?.GetBoundingRectangle() ?? default; + protected override IReadOnlyList GetOrCreateChildrenCore() => Array.Empty(); + protected override bool HasKeyboardFocusCore() => false; + protected override bool IsContentElementCore() => false; + protected override bool IsControlElementCore() => false; + protected override bool IsEnabledCore() => true; + protected override bool IsKeyboardFocusableCore() => false; + protected override void SetFocusCore() { } + protected override bool ShowContextMenuCore() => false; + protected internal override bool TrySetParent(AutomationPeer? parent) => false; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs new file mode 100644 index 00000000000..41778457e80 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs @@ -0,0 +1,39 @@ +using System; +using Avalonia.Automation.Platform; +using Avalonia.Controls; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class WindowAutomationPeer : WindowBaseAutomationPeer + { + public WindowAutomationPeer(IAutomationNodeFactory factory, Window owner) + : base(factory, owner) + { + if (owner.IsVisible) + StartTrackingFocus(); + else + owner.Opened += OnOpened; + owner.Closed += OnClosed; + } + + public new Window Owner => (Window)base.Owner; + + protected override string GetNameCore() => Owner.Title; + + private void OnOpened(object sender, EventArgs e) + { + Owner.Opened -= OnOpened; + StartTrackingFocus(); + } + + private void OnClosed(object sender, EventArgs e) + { + Owner.Closed -= OnClosed; + StopTrackingFocus(); + } + } +} + + diff --git a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs new file mode 100644 index 00000000000..ac235b38988 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs @@ -0,0 +1,72 @@ +using System.ComponentModel; +using Avalonia.Automation.Platform; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Platform; +using Avalonia.VisualTree; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class WindowBaseAutomationPeer : ControlAutomationPeer, IRootProvider + { + private Control? _focus; + + public WindowBaseAutomationPeer(IAutomationNodeFactory factory, WindowBase owner) + : base(factory, owner) + { + } + + public new WindowBase Owner => (WindowBase)base.Owner; + public ITopLevelImpl PlatformImpl => Owner.PlatformImpl; + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Window; + } + + public AutomationPeer? GetFocus() => _focus is object ? GetOrCreatePeer(_focus) : null; + + public AutomationPeer? GetPeerFromPoint(Point p) + { + var hit = Owner.GetVisualAt(p)?.FindAncestorOfType(includeSelf: true); + return hit is object ? GetOrCreatePeer(hit) : null; + } + + protected void StartTrackingFocus() + { + KeyboardDevice.Instance.PropertyChanged += KeyboardDevicePropertyChanged; + OnFocusChanged(KeyboardDevice.Instance.FocusedElement); + } + + protected void StopTrackingFocus() + { + KeyboardDevice.Instance.PropertyChanged -= KeyboardDevicePropertyChanged; + } + + private void OnFocusChanged(IInputElement? focus) + { + var oldFocus = _focus; + + _focus = focus?.VisualRoot == Owner ? focus as Control : null; + + if (_focus != oldFocus) + { + var peer = _focus is object ? GetOrCreatePeer(_focus) : null; + ((IRootAutomationNode)Node).FocusChanged(peer); + } + } + + private void KeyboardDevicePropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(KeyboardDevice.FocusedElement)) + { + OnFocusChanged(KeyboardDevice.Instance.FocusedElement); + } + } + } +} + + diff --git a/src/Avalonia.Controls/Automation/Platform/IAutomationNode.cs b/src/Avalonia.Controls/Automation/Platform/IAutomationNode.cs new file mode 100644 index 00000000000..cae3504a4a8 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Platform/IAutomationNode.cs @@ -0,0 +1,32 @@ +using System; +using Avalonia.Automation.Peers; + +#nullable enable + +namespace Avalonia.Automation.Platform +{ + /// + /// Represents a platform implementation of a node in the UI Automation tree. + /// + public interface IAutomationNode + { + /// + /// Gets a factory which can be used to create child nodes. + /// + IAutomationNodeFactory Factory { get; } + + /// + /// Called by the when the children of the peer change. + /// + void ChildrenChanged(); + + /// + /// Called by the when a property other than the parent, + /// children or root changes. + /// + /// The property that changed. + /// The previous value of the property. + /// The new value of the property. + void PropertyChanged(AutomationProperty property, object? oldValue, object? newValue); + } +} diff --git a/src/Avalonia.Controls/Automation/Platform/IAutomationNodeFactory.cs b/src/Avalonia.Controls/Automation/Platform/IAutomationNodeFactory.cs new file mode 100644 index 00000000000..da16611b9e5 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Platform/IAutomationNodeFactory.cs @@ -0,0 +1,18 @@ +using Avalonia.Automation.Peers; + +#nullable enable + +namespace Avalonia.Automation.Platform +{ + /// + /// Creates nodes in the UI Automation tree of the underlying platform. + /// + public interface IAutomationNodeFactory + { + /// + /// Creates an automation node for a peer. + /// + /// The peer. + IAutomationNode CreateNode(AutomationPeer peer); + } +} diff --git a/src/Avalonia.Controls/Automation/Platform/IRootAutomationNode.cs b/src/Avalonia.Controls/Automation/Platform/IRootAutomationNode.cs new file mode 100644 index 00000000000..8346a908c26 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Platform/IRootAutomationNode.cs @@ -0,0 +1,20 @@ +using Avalonia.Automation.Peers; + +#nullable enable + +namespace Avalonia.Automation.Platform +{ + /// + /// Represents a platform implementation of a root node in the UI Automation tree. + /// + public interface IRootAutomationNode : IAutomationNode + { + /// + /// Called by the when its focus changes. + /// + /// + /// The automation peer for the newly focused control or null if no control is focused. + /// + void FocusChanged(AutomationPeer? focus); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IExpandCollapseProvider.cs b/src/Avalonia.Controls/Automation/Provider/IExpandCollapseProvider.cs new file mode 100644 index 00000000000..f0308c226b4 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IExpandCollapseProvider.cs @@ -0,0 +1,24 @@ +namespace Avalonia.Automation.Provider +{ + /// + /// Exposes methods and properties to support UI Automation client access to controls that + /// visually expand to display content and collapse to hide content. + /// + public interface IExpandCollapseProvider + { + /// + /// Gets the state, expanded or collapsed, of the control. + /// + ExpandCollapseState ExpandCollapseState { get; } + + /// + /// Displays all child nodes, controls, or content of the control. + /// + void Expand(); + + /// + /// Hides all nodes, controls, or content that are descendants of the control. + /// + void Collapse(); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IInvokeProvider.cs b/src/Avalonia.Controls/Automation/Provider/IInvokeProvider.cs new file mode 100644 index 00000000000..47d7211c929 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IInvokeProvider.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Automation.Provider +{ + /// + /// Exposes methods and properties to support UI Automation client access to controls that + /// initiate or perform a single, unambiguous action and do not maintain state when + /// activated. + /// + public interface IInvokeProvider + { + /// + /// Sends a request to activate a control and initiate its single, unambiguous action. + /// + void Invoke(); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs b/src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs new file mode 100644 index 00000000000..d4cd35fcf9b --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs @@ -0,0 +1,37 @@ +#nullable enable + +namespace Avalonia.Automation.Provider +{ + /// + /// Exposes methods and properties to support access by a UI Automation client to controls + /// that can be set to a value within a range. + /// + public interface IRangeValueProvider + { + /// + /// Gets a value that indicates whether the value of a control is read-only. + /// + bool IsReadOnly { get; } + + /// + /// Gets the minimum range value that is supported by the control. + /// + double Minimum { get; } + + /// + /// Gets the maximum range value that is supported by the control. + /// + double Maximum { get; } + + /// + /// Gets the value of the control. + /// + double Value { get; } + + /// + /// Sets the value of the control. + /// + /// The value to set. + public void SetValue(double value); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs new file mode 100644 index 00000000000..698b4f26b9f --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs @@ -0,0 +1,14 @@ +using Avalonia.Automation.Peers; +using Avalonia.Platform; + +#nullable enable + +namespace Avalonia.Automation.Provider +{ + public interface IRootProvider + { + ITopLevelImpl? PlatformImpl { get; } + AutomationPeer? GetFocus(); + AutomationPeer? GetPeerFromPoint(Point p); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IScrollProvider.cs b/src/Avalonia.Controls/Automation/Provider/IScrollProvider.cs new file mode 100644 index 00000000000..1055a2f1e11 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IScrollProvider.cs @@ -0,0 +1,71 @@ +namespace Avalonia.Automation.Provider +{ + public enum ScrollAmount + { + LargeDecrement, + SmallDecrement, + NoAmount, + LargeIncrement, + SmallIncrement, + } + + /// + /// Exposes methods and properties to support access by a UI Automation client to a control + /// that acts as a scrollable container for a collection of child objects. + /// + public interface IScrollProvider + { + /// + /// Gets a value that indicates whether the control can scroll horizontally. + /// + bool HorizontallyScrollable { get; } + + /// + /// Gets the current horizontal scroll position. + /// + double HorizontalScrollPercent { get; } + + /// + /// Gets the current horizontal view size. + /// + double HorizontalViewSize { get; } + + /// + /// Gets a value that indicates whether the control can scroll vertically. + /// + bool VerticallyScrollable { get; } + + /// + /// Gets the current vertical scroll position. + /// + double VerticalScrollPercent { get; } + + /// + /// Gets the vertical view size. + /// + double VerticalViewSize { get; } + + /// + /// Scrolls the visible region of the content area horizontally and vertically. + /// + /// The horizontal increment specific to the control. + /// The vertical increment specific to the control. + void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount); + + /// + /// Sets the horizontal and vertical scroll position as a percentage of the total content + /// area within the control. + /// + /// + /// The horizontal position as a percentage of the content area's total range. + /// should be passed in if the control + /// cannot be scrolled in this direction. + /// + /// + /// The vertical position as a percentage of the content area's total range. + /// should be passed in if the control + /// cannot be scrolled in this direction. + /// + void SetScrollPercent(double horizontalPercent, double verticalPercent); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/ISelectionItemProvider .cs b/src/Avalonia.Controls/Automation/Provider/ISelectionItemProvider .cs new file mode 100644 index 00000000000..767d6bd7a2d --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/ISelectionItemProvider .cs @@ -0,0 +1,37 @@ +#nullable enable + +namespace Avalonia.Automation.Provider +{ + /// + /// Exposes methods and properties to support access by a UI Automation client to individual, + /// selectable child controls of containers that implement . + /// + public interface ISelectionItemProvider + { + /// + /// Gets a value that indicates whether an item is selected. + /// + bool IsSelected { get; } + + /// + /// Gets the UI Automation provider that implements and + /// acts as the container for the calling object. + /// + ISelectionProvider? SelectionContainer { get; } + + /// + /// Adds the current element to the collection of selected items. + /// + void AddToSelection(); + + /// + /// Removes the current element from the collection of selected items. + /// + void RemoveFromSelection(); + + /// + /// Clears any existing selection and then selects the current element. + /// + void Select(); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/ISelectionProvider.cs b/src/Avalonia.Controls/Automation/Provider/ISelectionProvider.cs new file mode 100644 index 00000000000..bf21c0151f3 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/ISelectionProvider.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using Avalonia.Automation.Peers; + +namespace Avalonia.Automation.Provider +{ + /// + /// Exposes methods and properties to support access by a UI Automation client to controls + /// that act as containers for a collection of individual, selectable child items. + /// + public interface ISelectionProvider + { + /// + /// Gets a value that indicates whether the provider allows more than one child element + /// to be selected concurrently. + /// + bool CanSelectMultiple { get; } + + /// + /// Gets a value that indicates whether the provider requires at least one child element + /// to be selected. + /// + bool IsSelectionRequired { get; } + + /// + /// Retrieves a provider for each child element that is selected. + /// + IReadOnlyList GetSelection(); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IToggleProvider.cs b/src/Avalonia.Controls/Automation/Provider/IToggleProvider.cs new file mode 100644 index 00000000000..67913e32048 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IToggleProvider.cs @@ -0,0 +1,40 @@ +namespace Avalonia.Automation.Provider +{ + /// + /// Contains values that specify the toggle state of a UI Automation element. + /// + public enum ToggleState + { + /// + /// The UI Automation element isn't selected, checked, marked, or otherwise activated. + /// + Off, + + /// + /// The UI Automation element is selected, checked, marked, or otherwise activated. + /// + On, + + /// + /// The UI Automation element is in an indeterminate state. + /// + Indeterminate, + } + + /// + /// Exposes methods and properties to support UI Automation client access to controls that can + /// cycle through a set of states and maintain a particular state. + /// + public interface IToggleProvider + { + /// + /// Gets the toggle state of the control. + /// + ToggleState ToggleState { get; } + + /// + /// Cycles through the toggle states of a control. + /// + void Toggle(); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IValueProvider.cs b/src/Avalonia.Controls/Automation/Provider/IValueProvider.cs new file mode 100644 index 00000000000..83dbde4ec26 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IValueProvider.cs @@ -0,0 +1,31 @@ +#nullable enable + +namespace Avalonia.Automation.Provider +{ + /// + /// Exposes methods and properties to support access by a UI Automation client to controls + /// that have an intrinsic value that does not span a range and that can be represented as + /// a string. + /// + public interface IValueProvider + { + /// + /// Gets a value that indicates whether the value of a control is read-only. + /// + bool IsReadOnly { get; } + + /// + /// Gets the value of the control. + /// + public string? Value { get; } + + /// + /// Sets the value of a control. + /// + /// + /// The value to set. The provider is responsible for converting the value to the + /// appropriate data type. + /// + public void SetValue(string? value); + } +} diff --git a/src/Avalonia.Controls/Automation/RangeValuePatternIdentifiers.cs b/src/Avalonia.Controls/Automation/RangeValuePatternIdentifiers.cs new file mode 100644 index 00000000000..e3fa744cc02 --- /dev/null +++ b/src/Avalonia.Controls/Automation/RangeValuePatternIdentifiers.cs @@ -0,0 +1,30 @@ +using Avalonia.Automation.Provider; + +namespace Avalonia.Automation +{ + /// + /// Contains values used as identifiers by . + /// + public static class RangeValuePatternIdentifiers + { + /// + /// Identifies automation property. + /// + public static AutomationProperty IsReadOnlyProperty { get; } = new(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty MinimumProperty { get; } = new(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty MaximumProperty { get; } = new(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty ValueProperty { get; } = new(); + } +} diff --git a/src/Avalonia.Controls/Automation/ScrollPatternIdentifiers.cs b/src/Avalonia.Controls/Automation/ScrollPatternIdentifiers.cs new file mode 100644 index 00000000000..ad10b96e17a --- /dev/null +++ b/src/Avalonia.Controls/Automation/ScrollPatternIdentifiers.cs @@ -0,0 +1,45 @@ +using Avalonia.Automation.Provider; + +namespace Avalonia.Automation +{ + /// + /// Contains values used as identifiers by . + /// + public static class ScrollPatternIdentifiers + { + /// + /// Specifies that scrolling should not be performed. + /// + public const double NoScroll = -1; + + /// + /// Identifies automation property. + /// + public static AutomationProperty HorizontallyScrollableProperty { get; } = new(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty HorizontalScrollPercentProperty { get; } = new(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty HorizontalViewSizeProperty { get; } = new(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty VerticallyScrollableProperty { get; } = new(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty VerticalScrollPercentProperty { get; } = new(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty VerticalViewSizeProperty { get; } = new(); + } +} diff --git a/src/Avalonia.Controls/Automation/SelectionPatternIdentifiers.cs b/src/Avalonia.Controls/Automation/SelectionPatternIdentifiers.cs new file mode 100644 index 00000000000..ce4fdda7398 --- /dev/null +++ b/src/Avalonia.Controls/Automation/SelectionPatternIdentifiers.cs @@ -0,0 +1,25 @@ +using Avalonia.Automation.Provider; + +namespace Avalonia.Automation +{ + /// + /// Contains values used as identifiers by . + /// + public static class SelectionPatternIdentifiers + { + /// + /// Identifies automation property. + /// + public static AutomationProperty CanSelectMultipleProperty { get; } = new(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty IsSelectionRequiredProperty { get; } = new(); + + /// + /// Identifies the property that gets the selected items in a container. + /// + public static AutomationProperty SelectionProperty { get; } = new(); + } +} diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index c779e4b0cbf..c8720f7cee7 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -1,6 +1,8 @@ using System; using System.Linq; using System.Windows.Input; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input; @@ -338,6 +340,11 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs } } + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new ButtonAutomationPeer(factory, this); + } + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { base.UpdateDataValidation(property, value); @@ -354,6 +361,8 @@ protected override void UpdateDataValidation(AvaloniaProperty property, Bi } } + internal void PerformClick() => OnClick(); + /// /// Called when the property changes. /// diff --git a/src/Avalonia.Controls/CheckBox.cs b/src/Avalonia.Controls/CheckBox.cs index 05d49a44b1a..374ab33338c 100644 --- a/src/Avalonia.Controls/CheckBox.cs +++ b/src/Avalonia.Controls/CheckBox.cs @@ -1,3 +1,5 @@ +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Primitives; namespace Avalonia.Controls @@ -7,5 +9,9 @@ namespace Avalonia.Controls /// public class CheckBox : ToggleButton { + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new CheckBoxAutomationPeer(factory, this); + } } } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index c5af5ffa7a7..83b97cc3d6b 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -1,5 +1,7 @@ using System; using System.Linq; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; @@ -335,6 +337,11 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) _popup.Opened += PopupOpened; } + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new ComboBoxAutomationPeer(factory, this); + } + internal void ItemFocused(ComboBoxItem dropDownItem) { if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 4aab92c428b..6b63ac14ccf 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -1,5 +1,7 @@ using System; using System.ComponentModel; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; @@ -48,6 +50,7 @@ public class Control : InputElement, IControl, INamed, IVisualBrushInitialize, I private DataTemplates? _dataTemplates; private IControl? _focusAdorner; + private AutomationPeer? _automationPeer; /// /// Gets or sets the control's focus adorner. @@ -188,5 +191,23 @@ protected override void OnLostFocus(RoutedEventArgs e) _focusAdorner = null; } } + + protected virtual AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new NoneAutomationPeer(factory, this); + } + + internal AutomationPeer GetOrCreateAutomationPeer(IAutomationNodeFactory factory) + { + VerifyAccess(); + + if (_automationPeer is object) + { + return _automationPeer; + } + + _automationPeer = OnCreateAutomationPeer(factory); + return _automationPeer; + } } } diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index 5fc7d8b6b6a..35bb8316f68 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -1,3 +1,5 @@ +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Media; using Avalonia.Media.Imaging; @@ -122,5 +124,10 @@ protected override Size ArrangeOverride(Size finalSize) return new Size(); } } + + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new ImageAutomationPeer(factory, this); + } } } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 4dc8aec6f31..b6c2e8656dc 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Collections.Specialized; using Avalonia.Collections; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Generators; using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; @@ -323,6 +325,11 @@ protected override void OnKeyDown(KeyEventArgs e) base.OnKeyDown(e); } + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new ItemsControlAutomationPeer(factory, this); + } + /// /// Called when the property changes. /// diff --git a/src/Avalonia.Controls/ListBoxItem.cs b/src/Avalonia.Controls/ListBoxItem.cs index 4fe5f4de40d..5599a89b62e 100644 --- a/src/Avalonia.Controls/ListBoxItem.cs +++ b/src/Avalonia.Controls/ListBoxItem.cs @@ -1,6 +1,7 @@ +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; -using Avalonia.Input; namespace Avalonia.Controls { @@ -34,5 +35,10 @@ public bool IsSelected get { return GetValue(IsSelectedProperty); } set { SetValue(IsSelectedProperty, value); } } + + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new ListItemAutomationPeer(factory, this); + } } } diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index 4da044fec1d..52605613936 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -1,3 +1,5 @@ +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -92,5 +94,10 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) inputRoot.AccessKeyHandler.MainMenu = this; } } + + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new MenuAutomationPeer(factory, this); + } } } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 94099a970e9..71bc08890e3 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -3,6 +3,8 @@ using System.Linq; using System.Reactive.Linq; using System.Windows.Input; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Generators; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; @@ -477,6 +479,11 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) } } + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new MenuItemAutomationPeer(factory, this); + } + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { base.UpdateDataValidation(property, value); diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index b445de04721..1837b90c5dd 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -1,6 +1,8 @@ using System; using System.Linq; using System.Reactive.Disposables; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; @@ -596,7 +598,17 @@ private void CloseCore() { if (PlacementTarget != null) { - FocusManager.Instance?.Focus(PlacementTarget); + var e = (IControl?)PlacementTarget; + + while (e is object && (!e.Focusable || !e.IsEffectivelyEnabled || !e.IsVisible)) + { + e = e.Parent; + } + + if (e is object) + { + FocusManager.Instance?.Focus(e); + } } else { diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index da7352b77f3..73dd68e7cbc 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Reactive.Disposables; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Interactivity; using Avalonia.Media; @@ -168,5 +170,10 @@ protected override sealed Size ArrangeSetBounds(Size size) return ClientSize; } } + + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new PopupRootAutomationPeer(factory, this); + } } } diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index 6b2c566422b..434d34928fd 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -1,4 +1,6 @@ using System; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Interactivity; @@ -169,6 +171,11 @@ protected virtual void OnIndeterminate(RoutedEventArgs e) RaiseEvent(e); } + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new ToggleButtonAutomationPeer(factory, this); + } + private void OnIsCheckedChanged(AvaloniaPropertyChangedEventArgs e) { var newValue = (bool?)e.NewValue; diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 6b75149d623..fe6e03e7a42 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -1,5 +1,7 @@ using System; using System.Reactive.Linq; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -688,6 +690,11 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) _scrollBarExpandSubscription = SubscribeToScrollBars(e); } + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new ScrollViewerAutomationPeer(factory, this); + } + private IDisposable SubscribeToScrollBars(TemplateAppliedEventArgs e) { static IObservable GetExpandedObservable(ScrollBar scrollBar) diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index e02efc2bd22..a3d18d73a18 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -1,5 +1,7 @@ using System; using Avalonia.Collections; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; @@ -209,6 +211,11 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) _pointerMovedDispose = this.AddDisposableHandler(PointerMovedEvent, TrackMoved, RoutingStrategies.Tunnel); } + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new SliderAutomationPeer(factory, this); + } + protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 306a9d3e6ac..63c68b783c3 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -1,6 +1,8 @@ using System.ComponentModel; using System.Linq; using Avalonia.Collections; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; @@ -230,5 +232,10 @@ protected override void OnPointerReleased(PointerReleasedEventArgs e) } } } + + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new TabControlAutomationPeer(factory, this); + } } } diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 593643a1eb1..4d50ef961d6 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -1,3 +1,5 @@ +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; @@ -80,5 +82,10 @@ private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj) } } } + + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new ListItemAutomationPeer(factory, this); + } } } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 31517ba59d7..eb23878c3d0 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -1,9 +1,11 @@ using System.Reactive.Linq; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; +using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; -using Avalonia.Layout; namespace Avalonia.Controls { @@ -532,6 +534,11 @@ protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e InvalidateMeasure(); } + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new TextAutomationPeer(factory, this); + } + private static bool IsValidMaxLines(int maxLines) => maxLines >= 0; private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0; diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 54d3af9b597..e91d3dc44d5 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -14,6 +14,8 @@ using Avalonia.Layout; using Avalonia.Utilities; using Avalonia.Controls.Metadata; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; namespace Avalonia.Controls { @@ -904,6 +906,11 @@ protected override void OnPointerReleased(PointerReleasedEventArgs e) } } + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new TextBoxAutomationPeer(factory, this); + } + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { if (property == TextProperty) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 98f4cadc13b..76481624933 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -4,6 +4,8 @@ using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Interactivity; @@ -965,5 +967,10 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs } } } + + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new WindowAutomationPeer(factory, this); + } } } diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 5899824c295..79cda870013 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -24,30 +24,7 @@ public class KeyboardDevice : IKeyboardDevice, INotifyPropertyChanged // the source of truth about the input focus is in KeyboardDevice private readonly TextInputMethodManager _textInputManager = new TextInputMethodManager(); - public IInputElement? FocusedElement - { - get - { - return _focusedElement; - } - - private set - { - _focusedElement = value; - - if (_focusedElement != null && _focusedElement.IsAttachedToVisualTree) - { - _focusedRoot = _focusedElement.VisualRoot as IInputRoot; - } - else - { - _focusedRoot = null; - } - - RaisePropertyChanged(); - _textInputManager.SetFocusedElement(value); - } - } + public IInputElement? FocusedElement => _focusedElement; private void ClearFocusWithinAncestors(IInputElement? element) { @@ -162,8 +139,8 @@ public void SetFocusedElement( } SetIsFocusWithin(FocusedElement, element); - - FocusedElement = element; + _focusedElement = element; + _focusedRoot = _focusedElement?.VisualRoot as IInputRoot; interactive?.RaiseEvent(new RoutedEventArgs { @@ -178,6 +155,9 @@ public void SetFocusedElement( NavigationMethod = method, KeyModifiers = keyModifiers, }); + + _textInputManager.SetFocusedElement(element); + RaisePropertyChanged(nameof(FocusedElement)); } } diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs new file mode 100644 index 00000000000..34ae969afe2 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs @@ -0,0 +1,19 @@ +using Avalonia.Automation; +using Avalonia.Automation.Provider; +using UIA = Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal partial class AutomationNode : UIA.IExpandCollapseProvider + { + public ExpandCollapseState ExpandCollapseState + { + get => InvokeSync(x => x.ExpandCollapseState); + } + + public void Expand() => InvokeSync(x => x.Expand()); + public void Collapse() => InvokeSync(x => x.Collapse()); + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs new file mode 100644 index 00000000000..d7e97cb30ca --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs @@ -0,0 +1,19 @@ +using Avalonia.Automation.Provider; +using UIA = Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal partial class AutomationNode : UIA.IRangeValueProvider + { + double UIA.IRangeValueProvider.Value => InvokeSync(x => x.Value); + public bool IsReadOnly => InvokeSync(x => x.IsReadOnly); + public double Maximum => InvokeSync(x => x.Maximum); + public double Minimum => InvokeSync(x => x.Minimum); + public double LargeChange => 1; + public double SmallChange => 1; + + public void SetValue(double value) => InvokeSync(x => x.SetValue(value)); + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs new file mode 100644 index 00000000000..55f1aba71cc --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs @@ -0,0 +1,32 @@ +using Avalonia.Automation.Provider; +using UIA = Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal partial class AutomationNode : UIA.IScrollProvider, UIA.IScrollItemProvider + { + public bool HorizontallyScrollable => InvokeSync(x => x.HorizontallyScrollable); + public double HorizontalScrollPercent => InvokeSync(x => x.HorizontalScrollPercent); + public double HorizontalViewSize => InvokeSync(x => x.HorizontalViewSize); + public bool VerticallyScrollable => InvokeSync(x => x.VerticallyScrollable); + public double VerticalScrollPercent => InvokeSync(x => x.VerticalScrollPercent); + public double VerticalViewSize => InvokeSync(x => x.VerticalViewSize); + + public void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount) + { + InvokeSync(x => x.Scroll(horizontalAmount, verticalAmount)); + } + + public void SetScrollPercent(double horizontalPercent, double verticalPercent) + { + InvokeSync(x => x.SetScrollPercent(horizontalPercent, verticalPercent)); + } + + public void ScrollIntoView() + { + InvokeSync(() => Peer.BringIntoView()); + } + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs new file mode 100644 index 00000000000..17634ae8050 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; +using UIA = Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal partial class AutomationNode : UIA.ISelectionProvider, UIA.ISelectionItemProvider + { + public bool CanSelectMultiple => InvokeSync(x => x.CanSelectMultiple); + public bool IsSelectionRequired => InvokeSync(x => x.IsSelectionRequired); + public bool IsSelected => InvokeSync(x => x.IsSelected); + + public UIA.IRawElementProviderSimple? SelectionContainer + { + get + { + var peer = InvokeSync(x => x.SelectionContainer); + return (peer as AutomationPeer)?.Node as AutomationNode; + } + } + + public UIA.IRawElementProviderSimple[] GetSelection() + { + var peers = InvokeSync>(x => x.GetSelection()); + return peers?.Select(x => (UIA.IRawElementProviderSimple)x.Node).ToArray() ?? + Array.Empty(); + } + + public void AddToSelection() => InvokeSync(x => x.AddToSelection()); + public void RemoveFromSelection() => InvokeSync(x => x.RemoveFromSelection()); + public void Select() => InvokeSync(x => x.Select()); + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs new file mode 100644 index 00000000000..90475557852 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs @@ -0,0 +1,13 @@ +using Avalonia.Automation.Provider; +using UIA = Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal partial class AutomationNode : UIA.IToggleProvider + { + public ToggleState ToggleState => InvokeSync(x => x.ToggleState); + public void Toggle() => InvokeSync(x => x.Toggle()); + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs new file mode 100644 index 00000000000..06e67086631 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs @@ -0,0 +1,18 @@ +using System.Runtime.InteropServices; +using Avalonia.Automation.Provider; +using UIA = Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal partial class AutomationNode : UIA.IValueProvider + { + public string? Value => InvokeSync(x => x.Value); + + public void SetValue([MarshalAs(UnmanagedType.LPWStr)] string? value) + { + InvokeSync(x => x.SetValue(value)); + } + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs new file mode 100644 index 00000000000..ca237567c1b --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -0,0 +1,328 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; +using Avalonia.Threading; +using Avalonia.Win32.Interop.Automation; +using AAP = Avalonia.Automation.Provider; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + [ComVisible(true)] + internal partial class AutomationNode : MarshalByRefObject, + IAutomationNode, + IRawElementProviderSimple, + IRawElementProviderSimple2, + IRawElementProviderFragment, + IRawElementProviderAdviseEvents, + IInvokeProvider + { + private static Dictionary s_propertyMap = new() + { + { AutomationElementIdentifiers.BoundingRectangleProperty, UiaPropertyId.BoundingRectangle }, + { AutomationElementIdentifiers.ClassNameProperty, UiaPropertyId.ClassName }, + { AutomationElementIdentifiers.NameProperty, UiaPropertyId.Name }, + { ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, UiaPropertyId.ExpandCollapseExpandCollapseState }, + { RangeValuePatternIdentifiers.IsReadOnlyProperty, UiaPropertyId.RangeValueIsReadOnly}, + { RangeValuePatternIdentifiers.MaximumProperty, UiaPropertyId.RangeValueMaximum }, + { RangeValuePatternIdentifiers.MinimumProperty, UiaPropertyId.RangeValueMinimum }, + { RangeValuePatternIdentifiers.ValueProperty, UiaPropertyId.RangeValueValue }, + { ScrollPatternIdentifiers.HorizontallyScrollableProperty, UiaPropertyId.ScrollHorizontallyScrollable }, + { ScrollPatternIdentifiers.HorizontalScrollPercentProperty, UiaPropertyId.ScrollHorizontalScrollPercent }, + { ScrollPatternIdentifiers.HorizontalViewSizeProperty, UiaPropertyId.ScrollHorizontalViewSize }, + { ScrollPatternIdentifiers.VerticallyScrollableProperty, UiaPropertyId.ScrollVerticallyScrollable }, + { ScrollPatternIdentifiers.VerticalScrollPercentProperty, UiaPropertyId.ScrollVerticalScrollPercent }, + { ScrollPatternIdentifiers.VerticalViewSizeProperty, UiaPropertyId.ScrollVerticalViewSize }, + { SelectionPatternIdentifiers.CanSelectMultipleProperty, UiaPropertyId.SelectionCanSelectMultiple }, + { SelectionPatternIdentifiers.IsSelectionRequiredProperty, UiaPropertyId.SelectionIsSelectionRequired }, + { SelectionPatternIdentifiers.SelectionProperty, UiaPropertyId.SelectionSelection }, + }; + + private readonly int[] _runtimeId; + private int _raiseFocusChanged; + private int _raisePropertyChanged; + + public AutomationNode(AutomationPeer peer) + { + _runtimeId = new int[] { 3, GetHashCode() }; + Peer = peer; + } + + public AutomationPeer Peer { get; } + public IAutomationNodeFactory Factory => AutomationNodeFactory.Instance; + + public Rect BoundingRectangle + { + get => InvokeSync(() => + { + if (GetRoot()?.Node is RootAutomationNode root) + return root.ToScreen(Peer.GetBoundingRectangle()); + return default; + }); + } + + public virtual IRawElementProviderFragmentRoot? FragmentRoot + { + get => InvokeSync(() => GetRoot())?.Node as IRawElementProviderFragmentRoot; + } + + public virtual IRawElementProviderSimple? HostRawElementProvider => null; + public ProviderOptions ProviderOptions => ProviderOptions.ServerSideProvider; + + public void ChildrenChanged() + { + UiaCoreProviderApi.UiaRaiseStructureChangedEvent( + this, + StructureChangeType.ChildrenInvalidated, + _runtimeId, + _runtimeId.Length); + } + + public void PropertyChanged(AutomationProperty property, object? oldValue, object? newValue) + { + if (_raisePropertyChanged > 0 && s_propertyMap.TryGetValue(property, out var id)) + { + UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent(this, (int)id, oldValue, newValue); + } + } + + [return: MarshalAs(UnmanagedType.IUnknown)] + public virtual object? GetPatternProvider(int patternId) + { + return (UiaPatternId)patternId switch + { + UiaPatternId.ExpandCollapse => Peer is IExpandCollapseProvider ? this : null, + UiaPatternId.Invoke => Peer is AAP.IInvokeProvider ? this : null, + UiaPatternId.RangeValue => Peer is AAP.IRangeValueProvider ? this : null, + UiaPatternId.Scroll => Peer is AAP.IScrollProvider ? this : null, + UiaPatternId.ScrollItem => this, + UiaPatternId.Selection => Peer is AAP.ISelectionProvider ? this : null, + UiaPatternId.SelectionItem => Peer is AAP.ISelectionItemProvider ? this : null, + UiaPatternId.Toggle => Peer is AAP.IToggleProvider ? this : null, + UiaPatternId.Value => Peer is AAP.IValueProvider ? this : null, + _ => null, + }; + } + + public virtual object? GetPropertyValue(int propertyId) + { + return (UiaPropertyId)propertyId switch + { + UiaPropertyId.AutomationId => InvokeSync(() => Peer.GetAutomationId()), + UiaPropertyId.ClassName => InvokeSync(() => Peer.GetClassName()), + UiaPropertyId.ClickablePoint => new[] { BoundingRectangle.Center.X, BoundingRectangle.Center.Y }, + UiaPropertyId.ControlType => InvokeSync(() => ToUiaControlType(Peer.GetAutomationControlType())), + UiaPropertyId.Culture => CultureInfo.CurrentCulture.LCID, + UiaPropertyId.FrameworkId => "Avalonia", + UiaPropertyId.HasKeyboardFocus => InvokeSync(() => Peer.HasKeyboardFocus()), + UiaPropertyId.IsContentElement => InvokeSync(() => Peer.IsContentElement()), + UiaPropertyId.IsControlElement => InvokeSync(() => Peer.IsControlElement()), + UiaPropertyId.IsEnabled => InvokeSync(() => Peer.IsEnabled()), + UiaPropertyId.IsKeyboardFocusable => InvokeSync(() => Peer.IsKeyboardFocusable()), + UiaPropertyId.LocalizedControlType => InvokeSync(() => Peer.GetLocalizedControlType()), + UiaPropertyId.Name => InvokeSync(() => Peer.GetName()), + UiaPropertyId.ProcessId => Process.GetCurrentProcess().Id, + UiaPropertyId.RuntimeId => _runtimeId, + _ => null, + }; + } + + public int[]? GetRuntimeId() => _runtimeId; + + public virtual IRawElementProviderFragment? Navigate(NavigateDirection direction) + { + IAutomationNode? GetSibling(int direction) + { + var children = Peer.GetParent()?.GetChildren(); + + for (var i = 0; i < (children?.Count ?? 0); ++i) + { + if (ReferenceEquals(children![i], Peer)) + { + var j = i + direction; + if (j >= 0 && j < children.Count) + return children[j].Node; + } + } + + return null; + } + + return InvokeSync(() => + { + return direction switch + { + NavigateDirection.Parent => Peer.GetParent()?.Node, + NavigateDirection.NextSibling => GetSibling(1), + NavigateDirection.PreviousSibling => GetSibling(-1), + NavigateDirection.FirstChild => Peer.GetChildren().FirstOrDefault()?.Node, + NavigateDirection.LastChild => Peer.GetChildren().LastOrDefault()?.Node, + _ => null, + }; + }) as IRawElementProviderFragment; + } + + public void SetFocus() => InvokeSync(() => Peer.SetFocus()); + + IRawElementProviderSimple[]? IRawElementProviderFragment.GetEmbeddedFragmentRoots() => null; + void IRawElementProviderSimple2.ShowContextMenu() => InvokeSync(() => Peer.ShowContextMenu()); + void IInvokeProvider.Invoke() => InvokeSync((AAP.IInvokeProvider x) => x.Invoke()); + + void IRawElementProviderAdviseEvents.AdviseEventAdded(int eventId, int[] properties) + { + switch ((UiaEventId)eventId) + { + case UiaEventId.AutomationPropertyChanged: + ++_raisePropertyChanged; + break; + case UiaEventId.AutomationFocusChanged: + ++_raiseFocusChanged; + break; + } + } + + void IRawElementProviderAdviseEvents.AdviseEventRemoved(int eventId, int[] properties) + { + switch ((UiaEventId)eventId) + { + case UiaEventId.AutomationPropertyChanged: + --_raisePropertyChanged; + break; + case UiaEventId.AutomationFocusChanged: + --_raiseFocusChanged; + break; + } + } + + protected void InvokeSync(Action action) + { + if (Dispatcher.UIThread.CheckAccess()) + action(); + else + Dispatcher.UIThread.InvokeAsync(action).Wait(); + } + + [return: MaybeNull] + protected T InvokeSync(Func func) + { + if (Dispatcher.UIThread.CheckAccess()) + return func(); + else + return Dispatcher.UIThread.InvokeAsync(func).Result; + } + + protected void InvokeSync(Action action) + { + if (Peer is TInterface i) + { + try + { + InvokeSync(() => action(i)); + } + catch (AggregateException e) when (e.InnerException is ElementNotEnabledException) + { + throw new COMException(e.Message, UiaCoreProviderApi.UIA_E_ELEMENTNOTENABLED); + } + } + } + + [return: MaybeNull] + protected TResult InvokeSync(Func func) + { + if (Peer is TInterface i) + { + try + { + return InvokeSync(() => func(i)); + } + catch (AggregateException e) when (e.InnerException is ElementNotEnabledException) + { + throw new COMException(e.Message, UiaCoreProviderApi.UIA_E_ELEMENTNOTENABLED); + } + } + + return default; + } + + protected void RaiseFocusChanged(AutomationNode? focused) + { + if (_raiseFocusChanged > 0) + { + UiaCoreProviderApi.UiaRaiseAutomationEvent( + focused, + (int)UiaEventId.AutomationFocusChanged); + } + } + + private AutomationPeer GetRoot() + { + Dispatcher.UIThread.VerifyAccess(); + + var peer = Peer; + var parent = peer.GetParent(); + + while (parent is object) + { + peer = parent; + parent = peer.GetParent(); + } + + return peer; + } + + private static UiaControlTypeId ToUiaControlType(AutomationControlType role) + { + return role switch + { + AutomationControlType.Button => UiaControlTypeId.Button, + AutomationControlType.Calendar => UiaControlTypeId.Calendar, + AutomationControlType.CheckBox => UiaControlTypeId.CheckBox, + AutomationControlType.ComboBox => UiaControlTypeId.ComboBox, + AutomationControlType.Edit => UiaControlTypeId.Edit, + AutomationControlType.Hyperlink => UiaControlTypeId.Hyperlink, + AutomationControlType.Image => UiaControlTypeId.Image, + AutomationControlType.ListItem => UiaControlTypeId.ListItem, + AutomationControlType.List => UiaControlTypeId.List, + AutomationControlType.Menu => UiaControlTypeId.Menu, + AutomationControlType.MenuBar => UiaControlTypeId.MenuBar, + AutomationControlType.MenuItem => UiaControlTypeId.MenuItem, + AutomationControlType.ProgressBar => UiaControlTypeId.ProgressBar, + AutomationControlType.RadioButton => UiaControlTypeId.RadioButton, + AutomationControlType.ScrollBar => UiaControlTypeId.ScrollBar, + AutomationControlType.Slider => UiaControlTypeId.Slider, + AutomationControlType.Spinner => UiaControlTypeId.Spinner, + AutomationControlType.StatusBar => UiaControlTypeId.StatusBar, + AutomationControlType.Tab => UiaControlTypeId.Tab, + AutomationControlType.TabItem => UiaControlTypeId.TabItem, + AutomationControlType.Text => UiaControlTypeId.Text, + AutomationControlType.ToolBar => UiaControlTypeId.ToolBar, + AutomationControlType.ToolTip => UiaControlTypeId.ToolTip, + AutomationControlType.Tree => UiaControlTypeId.Tree, + AutomationControlType.TreeItem => UiaControlTypeId.TreeItem, + AutomationControlType.Custom => UiaControlTypeId.Custom, + AutomationControlType.Group => UiaControlTypeId.Group, + AutomationControlType.Thumb => UiaControlTypeId.Thumb, + AutomationControlType.DataGrid => UiaControlTypeId.DataGrid, + AutomationControlType.DataItem => UiaControlTypeId.DataItem, + AutomationControlType.Document => UiaControlTypeId.Document, + AutomationControlType.SplitButton => UiaControlTypeId.SplitButton, + AutomationControlType.Window => UiaControlTypeId.Window, + AutomationControlType.Pane => UiaControlTypeId.Pane, + AutomationControlType.Header => UiaControlTypeId.Header, + AutomationControlType.HeaderItem => UiaControlTypeId.HeaderItem, + AutomationControlType.Table => UiaControlTypeId.Table, + AutomationControlType.TitleBar => UiaControlTypeId.TitleBar, + AutomationControlType.Separator => UiaControlTypeId.Separator, + _ => UiaControlTypeId.Custom, + }; + } + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNodeFactory.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNodeFactory.cs new file mode 100644 index 00000000000..776b65adc61 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNodeFactory.cs @@ -0,0 +1,20 @@ +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; +using Avalonia.Automation.Provider; +using Avalonia.Threading; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal class AutomationNodeFactory : IAutomationNodeFactory + { + public static readonly AutomationNodeFactory Instance = new AutomationNodeFactory(); + + public IAutomationNode CreateNode(AutomationPeer peer) + { + Dispatcher.UIThread.VerifyAccess(); + return peer is IRootProvider ? new RootAutomationNode(peer) : new AutomationNode(peer); + } + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs new file mode 100644 index 00000000000..cb8cfae90ed --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -0,0 +1,72 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; +using Avalonia.Automation.Provider; +using Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal class RootAutomationNode : AutomationNode, + IRawElementProviderFragmentRoot, + IRootAutomationNode + { + public RootAutomationNode(AutomationPeer peer) + : base(peer) + { + } + + public override IRawElementProviderFragmentRoot? FragmentRoot => this; + public new IRootProvider Peer => (IRootProvider)base.Peer; + public WindowImpl? WindowImpl => Peer.PlatformImpl as WindowImpl; + + public IRawElementProviderFragment? ElementProviderFromPoint(double x, double y) + { + if (WindowImpl is null) + return null; + + var p = WindowImpl.PointToClient(new PixelPoint((int)x, (int)y)); + var peer = (WindowBaseAutomationPeer)Peer; + var found = InvokeSync(() => peer.GetPeerFromPoint(p)); + var result = found?.Node as IRawElementProviderFragment; + return result; + } + + public IRawElementProviderFragment? GetFocus() + { + var focus = InvokeSync(() => Peer.GetFocus()); + return (AutomationNode?)focus?.Node; + } + + public void FocusChanged(AutomationPeer? focus) + { + var node = focus?.Node as AutomationNode; + RaiseFocusChanged(node); + } + + public Rect ToScreen(Rect rect) + { + if (WindowImpl is null) + return default; + return new PixelRect( + WindowImpl.PointToScreen(rect.TopLeft), + WindowImpl.PointToScreen(rect.BottomRight)) + .ToRect(1); + } + + public override IRawElementProviderSimple? HostRawElementProvider + { + get + { + var handle = WindowImpl?.Handle.Handle ?? IntPtr.Zero; + if (handle == IntPtr.Zero) + return null; + var hr = UiaCoreProviderApi.UiaHostProviderFromHwnd(handle, out var result); + Marshal.ThrowExceptionForHR(hr); + return result; + } + } + } +} diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index fe5f806fbef..94d7ed66510 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -6,6 +6,9 @@ + + + diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IDockProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IDockProvider.cs new file mode 100644 index 00000000000..2787434d266 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IDockProvider.cs @@ -0,0 +1,26 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("70d46e77-e3a8-449d-913c-e30eb2afecdb")] + public enum DockPosition + { + Top, + Left, + Bottom, + Right, + Fill, + None + } + + [ComVisible(true)] + [Guid("159bc72c-4ad3-485e-9637-d7052edf0146")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IDockProvider + { + void SetDockPosition(DockPosition dockPosition); + DockPosition DockPosition { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IExpandCollapseProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IExpandCollapseProvider.cs new file mode 100644 index 00000000000..67be1e6c717 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IExpandCollapseProvider.cs @@ -0,0 +1,16 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Automation; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("d847d3a5-cab0-4a98-8c32-ecb45c59ad24")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IExpandCollapseProvider + { + void Expand(); + void Collapse(); + ExpandCollapseState ExpandCollapseState { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IGridItemProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IGridItemProvider.cs new file mode 100644 index 00000000000..f911c384722 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IGridItemProvider.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("d02541f1-fb81-4d64-ae32-f520f8a6dbd1")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IGridItemProvider + { + int Row { get; } + int Column { get; } + int RowSpan { get; } + int ColumnSpan { get; } + IRawElementProviderSimple ContainingGrid { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IGridProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IGridProvider.cs new file mode 100644 index 00000000000..a8caf265247 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IGridProvider.cs @@ -0,0 +1,15 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("b17d6187-0907-464b-a168-0ef17a1572b1")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IGridProvider + { + IRawElementProviderSimple GetItem(int row, int column); + int RowCount { get; } + int ColumnCount { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IInvokeProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IInvokeProvider.cs new file mode 100644 index 00000000000..f35646d4561 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IInvokeProvider.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// Description: Invoke pattern provider interface + +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("54fcb24b-e18e-47a2-b4d3-eccbe77599a2")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IInvokeProvider + { + void Invoke(); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IMultipleViewProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IMultipleViewProvider.cs new file mode 100644 index 00000000000..c487a0f5df0 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IMultipleViewProvider.cs @@ -0,0 +1,16 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("6278cab1-b556-4a1a-b4e0-418acc523201")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IMultipleViewProvider + { + string GetViewName(int viewId); + void SetCurrentView(int viewId); + int CurrentView { get; } + int[] GetSupportedViews(); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IRangeValueProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IRangeValueProvider.cs new file mode 100644 index 00000000000..558f38a2cc6 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IRangeValueProvider.cs @@ -0,0 +1,19 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("36dc7aef-33e6-4691-afe1-2be7274b3d33")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IRangeValueProvider + { + void SetValue(double value); + double Value { get; } + bool IsReadOnly { [return: MarshalAs(UnmanagedType.Bool)] get; } + double Maximum { get; } + double Minimum { get; } + double LargeChange { get; } + double SmallChange { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderAdviseEvents.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderAdviseEvents.cs new file mode 100644 index 00000000000..1e799e05a22 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderAdviseEvents.cs @@ -0,0 +1,15 @@ +using System; +using System.Runtime.InteropServices; + + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("a407b27b-0f6d-4427-9292-473c7bf93258")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IRawElementProviderAdviseEvents : IRawElementProviderSimple + { + void AdviseEventAdded(int eventId, int [] properties); + void AdviseEventRemoved(int eventId, int [] properties); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragment.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragment.cs new file mode 100644 index 00000000000..a62aa842cb2 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragment.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.InteropServices; + +#nullable enable + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("670c3006-bf4c-428b-8534-e1848f645122")] + public enum NavigateDirection + { + Parent, + NextSibling, + PreviousSibling, + FirstChild, + LastChild, + } + + // NOTE: This interface needs to be public otherwise Navigate is never called. I have no idea + // why given that IRawElementProviderSimple and IRawElementProviderFragmentRoot seem to get + // called fine when they're internal, but I lost a couple of days to this. + [ComVisible(true)] + [Guid("f7063da8-8359-439c-9297-bbc5299a7d87")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IRawElementProviderFragment : IRawElementProviderSimple + { + IRawElementProviderFragment? Navigate(NavigateDirection direction); + int[]? GetRuntimeId(); + Rect BoundingRectangle { get; } + IRawElementProviderSimple[]? GetEmbeddedFragmentRoots(); + void SetFocus(); + IRawElementProviderFragmentRoot? FragmentRoot { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragmentRoot.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragmentRoot.cs new file mode 100644 index 00000000000..71d1bdce608 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragmentRoot.cs @@ -0,0 +1,14 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("620ce2a5-ab8f-40a9-86cb-de3c75599b58")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IRawElementProviderFragmentRoot : IRawElementProviderFragment + { + IRawElementProviderFragment ElementProviderFromPoint(double x, double y); + IRawElementProviderFragment GetFocus(); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple.cs new file mode 100644 index 00000000000..439036290ed --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple.cs @@ -0,0 +1,285 @@ +using System; +using System.Runtime.InteropServices; + +#nullable enable + +namespace Avalonia.Win32.Interop.Automation +{ + [Flags] + public enum ProviderOptions + { + ClientSideProvider = 0x0001, + ServerSideProvider = 0x0002, + NonClientAreaProvider = 0x0004, + OverrideProvider = 0x0008, + ProviderOwnsSetFocus = 0x0010, + UseComThreading = 0x0020 + } + + internal enum UiaPropertyId + { + RuntimeId = 30000, + BoundingRectangle, + ProcessId, + ControlType, + LocalizedControlType, + Name, + AcceleratorKey, + AccessKey, + HasKeyboardFocus, + IsKeyboardFocusable, + IsEnabled, + AutomationId, + ClassName, + HelpText, + ClickablePoint, + Culture, + IsControlElement, + IsContentElement, + LabeledBy, + IsPassword, + NativeWindowHandle, + ItemType, + IsOffscreen, + Orientation, + FrameworkId, + IsRequiredForForm, + ItemStatus, + IsDockPatternAvailable, + IsExpandCollapsePatternAvailable, + IsGridItemPatternAvailable, + IsGridPatternAvailable, + IsInvokePatternAvailable, + IsMultipleViewPatternAvailable, + IsRangeValuePatternAvailable, + IsScrollPatternAvailable, + IsScrollItemPatternAvailable, + IsSelectionItemPatternAvailable, + IsSelectionPatternAvailable, + IsTablePatternAvailable, + IsTableItemPatternAvailable, + IsTextPatternAvailable, + IsTogglePatternAvailable, + IsTransformPatternAvailable, + IsValuePatternAvailable, + IsWindowPatternAvailable, + ValueValue, + ValueIsReadOnly, + RangeValueValue, + RangeValueIsReadOnly, + RangeValueMinimum, + RangeValueMaximum, + RangeValueLargeChange, + RangeValueSmallChange, + ScrollHorizontalScrollPercent, + ScrollHorizontalViewSize, + ScrollVerticalScrollPercent, + ScrollVerticalViewSize, + ScrollHorizontallyScrollable, + ScrollVerticallyScrollable, + SelectionSelection, + SelectionCanSelectMultiple, + SelectionIsSelectionRequired, + GridRowCount, + GridColumnCount, + GridItemRow, + GridItemColumn, + GridItemRowSpan, + GridItemColumnSpan, + GridItemContainingGrid, + DockDockPosition, + ExpandCollapseExpandCollapseState, + MultipleViewCurrentView, + MultipleViewSupportedViews, + WindowCanMaximize, + WindowCanMinimize, + WindowWindowVisualState, + WindowWindowInteractionState, + WindowIsModal, + WindowIsTopmost, + SelectionItemIsSelected, + SelectionItemSelectionContainer, + TableRowHeaders, + TableColumnHeaders, + TableRowOrColumnMajor, + TableItemRowHeaderItems, + TableItemColumnHeaderItems, + ToggleToggleState, + TransformCanMove, + TransformCanResize, + TransformCanRotate, + IsLegacyIAccessiblePatternAvailable, + LegacyIAccessibleChildId, + LegacyIAccessibleName, + LegacyIAccessibleValue, + LegacyIAccessibleDescription, + LegacyIAccessibleRole, + LegacyIAccessibleState, + LegacyIAccessibleHelp, + LegacyIAccessibleKeyboardShortcut, + LegacyIAccessibleSelection, + LegacyIAccessibleDefaultAction, + AriaRole, + AriaProperties, + IsDataValidForForm, + ControllerFor, + DescribedBy, + FlowsTo, + ProviderDescription, + IsItemContainerPatternAvailable, + IsVirtualizedItemPatternAvailable, + IsSynchronizedInputPatternAvailable, + OptimizeForVisualContent, + IsObjectModelPatternAvailable, + AnnotationAnnotationTypeId, + AnnotationAnnotationTypeName, + AnnotationAuthor, + AnnotationDateTime, + AnnotationTarget, + IsAnnotationPatternAvailable, + IsTextPattern2Available, + StylesStyleId, + StylesStyleName, + StylesFillColor, + StylesFillPatternStyle, + StylesShape, + StylesFillPatternColor, + StylesExtendedProperties, + IsStylesPatternAvailable, + IsSpreadsheetPatternAvailable, + SpreadsheetItemFormula, + SpreadsheetItemAnnotationObjects, + SpreadsheetItemAnnotationTypes, + IsSpreadsheetItemPatternAvailable, + Transform2CanZoom, + IsTransformPattern2Available, + LiveSetting, + IsTextChildPatternAvailable, + IsDragPatternAvailable, + DragIsGrabbed, + DragDropEffect, + DragDropEffects, + IsDropTargetPatternAvailable, + DropTargetDropTargetEffect, + DropTargetDropTargetEffects, + DragGrabbedItems, + Transform2ZoomLevel, + Transform2ZoomMinimum, + Transform2ZoomMaximum, + FlowsFrom, + IsTextEditPatternAvailable, + IsPeripheral, + IsCustomNavigationPatternAvailable, + PositionInSet, + SizeOfSet, + Level, + AnnotationTypes, + AnnotationObjects, + LandmarkType, + LocalizedLandmarkType, + FullDescription, + FillColor, + OutlineColor, + FillType, + VisualEffects, + OutlineThickness, + CenterPoint, + Rotatation, + Size + } + + internal enum UiaPatternId + { + Invoke = 10000, + Selection, + Value, + RangeValue, + Scroll, + ExpandCollapse, + Grid, + GridItem, + MultipleView, + Window, + SelectionItem, + Dock, + Table, + TableItem, + Text, + Toggle, + Transform, + ScrollItem, + LegacyIAccessible, + ItemContainer, + VirtualizedItem, + SynchronizedInput, + ObjectModel, + Annotation, + Text2, + Styles, + Spreadsheet, + SpreadsheetItem, + Transform2, + TextChild, + Drag, + DropTarget, + TextEdit, + CustomNavigation + }; + + internal enum UiaControlTypeId + { + Button = 50000, + Calendar, + CheckBox, + ComboBox, + Edit, + Hyperlink, + Image, + ListItem, + List, + Menu, + MenuBar, + MenuItem, + ProgressBar, + RadioButton, + ScrollBar, + Slider, + Spinner, + StatusBar, + Tab, + TabItem, + Text, + ToolBar, + ToolTip, + Tree, + TreeItem, + Custom, + Group, + Thumb, + DataGrid, + DataItem, + Document, + SplitButton, + Window, + Pane, + Header, + HeaderItem, + Table, + TitleBar, + Separator, + SemanticZoom, + AppBar + }; + + [ComVisible(true)] + [Guid("d6dd68d1-86fd-4332-8666-9abedea2d24c")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IRawElementProviderSimple + { + ProviderOptions ProviderOptions { get; } + [return: MarshalAs(UnmanagedType.IUnknown)] + object? GetPatternProvider(int patternId); + object? GetPropertyValue(int propertyId); + IRawElementProviderSimple? HostRawElementProvider { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple2.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple2.cs new file mode 100644 index 00000000000..f3504b8d773 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple2.cs @@ -0,0 +1,14 @@ +using System; +using System.Runtime.InteropServices; + +#nullable enable + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("a0a839a9-8da1-4a82-806a-8e0d44e79f56")] + public interface IRawElementProviderSimple2 + { + void ShowContextMenu(); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IScrollItemProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IScrollItemProvider.cs new file mode 100644 index 00000000000..c34c8667ef8 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IScrollItemProvider.cs @@ -0,0 +1,13 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("2360c714-4bf1-4b26-ba65-9b21316127eb")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IScrollItemProvider + { + void ScrollIntoView(); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IScrollProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IScrollProvider.cs new file mode 100644 index 00000000000..154d52c6af6 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IScrollProvider.cs @@ -0,0 +1,21 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Automation.Provider; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("b38b8077-1fc3-42a5-8cae-d40c2215055a")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IScrollProvider + { + void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount); + void SetScrollPercent(double horizontalPercent, double verticalPercent); + double HorizontalScrollPercent { get; } + double VerticalScrollPercent { get; } + double HorizontalViewSize { get; } + double VerticalViewSize { get; } + bool HorizontallyScrollable { [return: MarshalAs(UnmanagedType.Bool)] get; } + bool VerticallyScrollable { [return: MarshalAs(UnmanagedType.Bool)] get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/ISelectionItemProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/ISelectionItemProvider.cs new file mode 100644 index 00000000000..1de0cf0f9b2 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/ISelectionItemProvider.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("2acad808-b2d4-452d-a407-91ff1ad167b2")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ISelectionItemProvider + { + void Select(); + void AddToSelection(); + void RemoveFromSelection(); + bool IsSelected { [return: MarshalAs(UnmanagedType.Bool)] get; } + IRawElementProviderSimple? SelectionContainer { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/ISelectionProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/ISelectionProvider.cs new file mode 100644 index 00000000000..8a5924126dc --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/ISelectionProvider.cs @@ -0,0 +1,15 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("fb8b03af-3bdf-48d4-bd36-1a65793be168")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ISelectionProvider + { + IRawElementProviderSimple [] GetSelection(); + bool CanSelectMultiple { [return: MarshalAs(UnmanagedType.Bool)] get; } + bool IsSelectionRequired { [return: MarshalAs(UnmanagedType.Bool)] get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/ISynchronizedInputProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/ISynchronizedInputProvider.cs new file mode 100644 index 00000000000..def1bbd4b96 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/ISynchronizedInputProvider.cs @@ -0,0 +1,26 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("fdc8f176-aed2-477a-8c89-5604c66f278d")] + public enum SynchronizedInputType + { + KeyUp = 0x01, + KeyDown = 0x02, + MouseLeftButtonUp = 0x04, + MouseLeftButtonDown = 0x08, + MouseRightButtonUp = 0x10, + MouseRightButtonDown = 0x20 + } + + [ComVisible(true)] + [Guid("29db1a06-02ce-4cf7-9b42-565d4fab20ee")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ISynchronizedInputProvider + { + void StartListening(SynchronizedInputType inputType); + void Cancel(); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/ITableItemProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/ITableItemProvider.cs new file mode 100644 index 00000000000..36751122d14 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/ITableItemProvider.cs @@ -0,0 +1,14 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("b9734fa6-771f-4d78-9c90-2517999349cd")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ITableItemProvider : IGridItemProvider + { + IRawElementProviderSimple [] GetRowHeaderItems(); + IRawElementProviderSimple [] GetColumnHeaderItems(); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/ITableProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/ITableProvider.cs new file mode 100644 index 00000000000..e82bda3272c --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/ITableProvider.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("15fdf2e2-9847-41cd-95dd-510612a025ea")] + public enum RowOrColumnMajor + { + RowMajor, + ColumnMajor, + Indeterminate, + } + + [ComVisible(true)] + [Guid("9c860395-97b3-490a-b52a-858cc22af166")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ITableProvider : IGridProvider + { + IRawElementProviderSimple [] GetRowHeaders(); + IRawElementProviderSimple [] GetColumnHeaders(); + RowOrColumnMajor RowOrColumnMajor { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/ITextProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/ITextProvider.cs new file mode 100644 index 00000000000..3f8fbc80c74 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/ITextProvider.cs @@ -0,0 +1,30 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [Flags] + [ComVisible(true)] + [Guid("3d9e3d8f-bfb0-484f-84ab-93ff4280cbc4")] + public enum SupportedTextSelection + { + None, + Single, + Multiple, + } + + [ComVisible(true)] + [Guid("3589c92c-63f3-4367-99bb-ada653b77cf2")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ITextProvider + { + ITextRangeProvider [] GetSelection(); + ITextRangeProvider [] GetVisibleRanges(); + ITextRangeProvider RangeFromChild(IRawElementProviderSimple childElement); + ITextRangeProvider RangeFromPoint(Point screenLocation); + ITextRangeProvider DocumentRange { get; } + SupportedTextSelection SupportedTextSelection { get; } + } +} + + diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/ITextRangeProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/ITextRangeProvider.cs new file mode 100644 index 00000000000..9ebb4c9f497 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/ITextRangeProvider.cs @@ -0,0 +1,48 @@ +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + public enum TextPatternRangeEndpoint + { + Start = 0, + End = 1, + } + + public enum TextUnit + { + Character = 0, + Format = 1, + Word = 2, + Line = 3, + Paragraph = 4, + Page = 5, + Document = 6, + } + + [ComVisible(true)] + [Guid("5347ad7b-c355-46f8-aff5-909033582f63")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ITextRangeProvider + + { + ITextRangeProvider Clone(); + [return: MarshalAs(UnmanagedType.Bool)] + bool Compare(ITextRangeProvider range); + int CompareEndpoints(TextPatternRangeEndpoint endpoint, ITextRangeProvider targetRange, TextPatternRangeEndpoint targetEndpoint); + void ExpandToEnclosingUnit(TextUnit unit); + ITextRangeProvider FindAttribute(int attribute, object value, [MarshalAs(UnmanagedType.Bool)] bool backward); + ITextRangeProvider FindText(string text, [MarshalAs(UnmanagedType.Bool)] bool backward, [MarshalAs(UnmanagedType.Bool)] bool ignoreCase); + object GetAttributeValue(int attribute); + double [] GetBoundingRectangles(); + IRawElementProviderSimple GetEnclosingElement(); + string GetText(int maxLength); + int Move(TextUnit unit, int count); + int MoveEndpointByUnit(TextPatternRangeEndpoint endpoint, TextUnit unit, int count); + void MoveEndpointByRange(TextPatternRangeEndpoint endpoint, ITextRangeProvider targetRange, TextPatternRangeEndpoint targetEndpoint); + void Select(); + void AddToSelection(); + void RemoveFromSelection(); + void ScrollIntoView([MarshalAs(UnmanagedType.Bool)] bool alignToTop); + IRawElementProviderSimple[] GetChildren(); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IToggleProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IToggleProvider.cs new file mode 100644 index 00000000000..e4072a12506 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IToggleProvider.cs @@ -0,0 +1,15 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Automation.Provider; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("56d00bd0-c4f4-433c-a836-1a52a57e0892")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IToggleProvider + { + void Toggle( ); + ToggleState ToggleState { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/ITransformProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/ITransformProvider.cs new file mode 100644 index 00000000000..4859f2d0781 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/ITransformProvider.cs @@ -0,0 +1,18 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("6829ddc4-4f91-4ffa-b86f-bd3e2987cb4c")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ITransformProvider + { + void Move( double x, double y ); + void Resize( double width, double height ); + void Rotate( double degrees ); + bool CanMove { [return: MarshalAs(UnmanagedType.Bool)] get; } + bool CanResize { [return: MarshalAs(UnmanagedType.Bool)] get; } + bool CanRotate { [return: MarshalAs(UnmanagedType.Bool)] get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IValueProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IValueProvider.cs new file mode 100644 index 00000000000..919be647f8d --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IValueProvider.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.InteropServices; + +#nullable enable + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("c7935180-6fb3-4201-b174-7df73adbf64a")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IValueProvider + { + void SetValue([MarshalAs(UnmanagedType.LPWStr)] string? value); + string? Value { get; } + bool IsReadOnly { [return: MarshalAs(UnmanagedType.Bool)] get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IWindowProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IWindowProvider.cs new file mode 100644 index 00000000000..d915beb601e --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IWindowProvider.cs @@ -0,0 +1,42 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("fdc8f176-aed2-477a-8c89-ea04cc5f278d")] + public enum WindowVisualState + { + Normal, + Maximized, + Minimized + } + + [ComVisible(true)] + [Guid("65101cc7-7904-408e-87a7-8c6dbd83a18b")] + public enum WindowInteractionState + { + Running, + Closing, + ReadyForUserInteraction, + BlockedByModalWindow, + NotResponding + } + + [ComVisible(true)] + [Guid("987df77b-db06-4d77-8f8a-86a9c3bb90b9")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IWindowProvider + { + void SetVisualState(WindowVisualState state); + void Close(); + [return: MarshalAs(UnmanagedType.Bool)] + bool WaitForInputIdle(int milliseconds); + bool Maximizable { [return: MarshalAs(UnmanagedType.Bool)] get; } + bool Minimizable { [return: MarshalAs(UnmanagedType.Bool)] get; } + bool IsModal { [return: MarshalAs(UnmanagedType.Bool)] get; } + WindowVisualState VisualState { get; } + WindowInteractionState InteractionState { get; } + bool IsTopmost { [return: MarshalAs(UnmanagedType.Bool)] get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreProviderApi.cs b/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreProviderApi.cs new file mode 100644 index 00000000000..4ba7a710d48 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreProviderApi.cs @@ -0,0 +1,91 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("d8e55844-7043-4edc-979d-593cc6b4775e")] + internal enum AsyncContentLoadedState + { + Beginning, + Progress, + Completed, + } + + [ComVisible(true)] + [Guid("e4cfef41-071d-472c-a65c-c14f59ea81eb")] + internal enum StructureChangeType + { + ChildAdded, + ChildRemoved, + ChildrenInvalidated, + ChildrenBulkAdded, + ChildrenBulkRemoved, + ChildrenReordered, + } + + internal enum UiaEventId + { + ToolTipOpened = 20000, + ToolTipClosed, + StructureChanged, + MenuOpened, + AutomationPropertyChanged, + AutomationFocusChanged, + AsyncContentLoaded, + MenuClosed, + LayoutInvalidated, + Invoke_Invoked, + SelectionItem_ElementAddedToSelection, + SelectionItem_ElementRemovedFromSelection, + SelectionItem_ElementSelected, + Selection_Invalidated, + Text_TextSelectionChanged, + Text_TextChanged, + Window_WindowOpened, + Window_WindowClosed, + MenuModeStart, + MenuModeEnd, + InputReachedTarget, + InputReachedOtherElement, + InputDiscarded, + SystemAlert, + LiveRegionChanged, + HostedFragmentRootsInvalidated, + Drag_DragStart, + Drag_DragCancel, + Drag_DragComplete, + DropTarget_DragEnter, + DropTarget_DragLeave, + DropTarget_Dropped, + TextEdit_TextChanged, + TextEdit_ConversionTargetChanged, + Changes + }; + + internal static class UiaCoreProviderApi + { + public const int UIA_E_ELEMENTNOTENABLED = unchecked((int)0x80040200); + + [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] + public static extern bool UiaClientsAreListening(); + + [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr UiaReturnRawElementProvider(IntPtr hwnd, IntPtr wParam, IntPtr lParam, IRawElementProviderSimple el); + + [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] + public static extern int UiaHostProviderFromHwnd(IntPtr hwnd, [MarshalAs(UnmanagedType.Interface)] out IRawElementProviderSimple provider); + + [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] + public static extern int UiaRaiseAutomationEvent(IRawElementProviderSimple provider, int id); + + [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] + public static extern int UiaRaiseAutomationPropertyChangedEvent(IRawElementProviderSimple provider, int id, object oldValue, object newValue); + + [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] + public static extern int UiaRaiseStructureChangedEvent(IRawElementProviderSimple provider, StructureChangeType structureChangeType, int[] runtimeId, int runtimeIdLen); + + [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] + public static extern int UiaDisconnectProvider(IRawElementProviderSimple provider); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs b/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs new file mode 100644 index 00000000000..4375b2fde18 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs @@ -0,0 +1,62 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + internal static class UiaCoreTypesApi + { + private const string StartListeningExportName = "SynchronizedInputPattern_StartListening"; + + internal enum AutomationIdType + { + Property, + Pattern, + Event, + ControlType, + TextAttribute + } + + internal const int UIA_E_ELEMENTNOTENABLED = unchecked((int)0x80040200); + internal const int UIA_E_ELEMENTNOTAVAILABLE = unchecked((int)0x80040201); + internal const int UIA_E_NOCLICKABLEPOINT = unchecked((int)0x80040202); + internal const int UIA_E_PROXYASSEMBLYNOTLOADED = unchecked((int)0x80040203); + + internal static int UiaLookupId(AutomationIdType type, ref Guid guid) + { + return RawUiaLookupId( type, ref guid ); + } + + internal static object UiaGetReservedNotSupportedValue() + { + object notSupportedValue; + CheckError(RawUiaGetReservedNotSupportedValue(out notSupportedValue)); + return notSupportedValue; + } + + internal static object UiaGetReservedMixedAttributeValue() + { + object mixedAttributeValue; + CheckError(RawUiaGetReservedMixedAttributeValue(out mixedAttributeValue)); + return mixedAttributeValue; + } + + private static void CheckError(int hr) + { + if (hr >= 0) + { + return; + } + + Marshal.ThrowExceptionForHR(hr, (IntPtr)(-1)); + } + + [DllImport("UIAutomationCore.dll", EntryPoint = "UiaLookupId", CharSet = CharSet.Unicode)] + private static extern int RawUiaLookupId(AutomationIdType type, ref Guid guid); + + [DllImport("UIAutomationCore.dll", EntryPoint = "UiaGetReservedNotSupportedValue", CharSet = CharSet.Unicode)] + private static extern int RawUiaGetReservedNotSupportedValue([MarshalAs(UnmanagedType.IUnknown)] out object notSupportedValue); + + [DllImport("UIAutomationCore.dll", EntryPoint = "UiaGetReservedMixedAttributeValue", CharSet = CharSet.Unicode)] + private static extern int RawUiaGetReservedMixedAttributeValue([MarshalAs(UnmanagedType.IUnknown)] out object mixedAttributeValue); + } +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index e7d16f731c8..b0d41b72eb6 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -2,10 +2,10 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using Avalonia.Controls; -using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Win32.Input; +using Avalonia.Win32.Interop.Automation; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32 @@ -17,6 +17,7 @@ public partial class WindowImpl protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { const double wheelDelta = 120.0; + const long UiaRootObjectId = -25; uint timestamp = unchecked((uint)GetMessageTime()); RawInputEventArgs e = null; @@ -453,6 +454,19 @@ protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam, case WindowsMessage.WM_KILLFOCUS: LostFocus?.Invoke(); break; + + case WindowsMessage.WM_GETOBJECT: + if ((long)lParam == UiaRootObjectId) + { + var provider = GetOrCreateAutomationProvider(); + + if (provider is object) + { + var r = UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, wParam, lParam, provider); + return r; + } + } + break; } #if USE_MANAGED_DRAG diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 1ddec2e763b..59927fd4889 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -3,6 +3,8 @@ using System.ComponentModel; using System.Runtime.InteropServices; using Avalonia.Controls; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; @@ -12,6 +14,7 @@ using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Win32.Automation; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; using Avalonia.Win32.OpenGl; @@ -24,7 +27,8 @@ namespace Avalonia.Win32 /// /// Window implementation for Win32 platform. /// - public partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo, + public partial class WindowImpl : IWindowImpl, + EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo, ITopLevelImplWithNativeControlHost { private static readonly List s_instances = new List(); @@ -83,6 +87,7 @@ public partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGl private POINT _maxTrackSize; private WindowImpl _parent; private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default; + private AutomationNode _automationProvider; private bool _isCloseRequested; public WindowImpl() @@ -727,7 +732,7 @@ private void CreateWindow() throw new Win32Exception(); } - Handle = new PlatformHandle(_hwnd, PlatformConstants.WindowHandleType); + Handle = new WindowImplPlatformHandle(this); _multitouch = Win32Platform.Options.EnableMultitouch ?? true; @@ -1250,6 +1255,17 @@ public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) /// public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0); + internal AutomationNode GetOrCreateAutomationProvider() + { + if (_automationProvider is null) + { + var peer = ControlAutomationPeer.GetOrCreatePeer(AutomationNodeFactory.Instance, (Control)_owner); + _automationProvider = peer.Node as AutomationNode; + } + + return _automationProvider; + } + private struct SavedWindowInfo { public WindowStyles Style { get; set; } @@ -1264,5 +1280,13 @@ private struct WindowProperties public SystemDecorations Decorations; public bool IsFullScreen; } + + private class WindowImplPlatformHandle : IPlatformHandle + { + private readonly WindowImpl _owner; + public WindowImplPlatformHandle(WindowImpl owner) => _owner = owner; + public IntPtr Handle => _owner.Hwnd; + public string HandleDescriptor => PlatformConstants.WindowHandleType; + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Automation/ControlAutomationPeerTests.cs b/tests/Avalonia.Controls.UnitTests/Automation/ControlAutomationPeerTests.cs new file mode 100644 index 00000000000..250cad4e253 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Automation/ControlAutomationPeerTests.cs @@ -0,0 +1,253 @@ +using System.Linq; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; +using Avalonia.Automation.Provider; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Automation +{ + public class ControlAutomationPeerTests + { + private static Mock _factory; + + public ControlAutomationPeerTests() + { + _factory = new Mock(); + _factory.Setup(x => x.CreateNode(It.IsAny())) + .Returns(() => Mock.Of(x => x.Factory == _factory)); + } + + public class Children + { + [Fact] + public void Creates_Children_For_Controls_In_Visual_Tree() + { + var panel = new Panel + { + Children = + { + new Border(), + new Border(), + }, + }; + + var factory = CreateFactory(); + var target = CreatePeer(factory, panel); + + Assert.Equal( + panel.GetVisualChildren(), + target.GetChildren().Cast().Select(x => x.Owner)); + } + + [Fact] + public void Creates_Children_when_Controls_Attached_To_Visual_Tree() + { + var contentControl = new ContentControl + { + Template = new FuncControlTemplate((o, ns) => + new ContentPresenter + { + Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = o[!ContentControl.ContentProperty], + }), + Content = new Border(), + }; + + var factory = CreateFactory(); + var target = CreatePeer(factory, contentControl); + + Assert.Empty(target.GetChildren()); + + contentControl.Measure(Size.Infinity); + + Assert.Equal(1, target.GetChildren().Count); + } + + [Fact] + public void Updates_Children_When_VisualChildren_Added() + { + var panel = new Panel + { + Children = + { + new Border(), + new Border(), + }, + }; + + var factory = CreateFactory(); + var target = CreatePeer(factory, panel); + var children = target.GetChildren(); + + Assert.Equal(2, children.Count); + + panel.Children.Add(new Decorator()); + + children = target.GetChildren(); + Assert.Equal(3, children.Count); + } + + [Fact] + public void Updates_Children_When_VisualChildren_Removed() + { + var panel = new Panel + { + Children = + { + new Border(), + new Border(), + }, + }; + + var factory = CreateFactory(); + var target = CreatePeer(factory, panel); + var children = target.GetChildren(); + + Assert.Equal(2, children.Count); + + panel.Children.RemoveAt(1); + + children = target.GetChildren(); + Assert.Equal(1, children.Count); + } + + [Fact] + public void Updates_Children_When_Visibility_Changes() + { + var panel = new Panel + { + Children = + { + new Border(), + new Border(), + }, + }; + + var factory = CreateFactory(); + var target = CreatePeer(factory, panel); + var children = target.GetChildren(); + + Assert.Equal(2, children.Count); + + panel.Children[1].IsVisible = false; + children = target.GetChildren(); + Assert.Equal(1, children.Count); + + panel.Children[1].IsVisible = true; + children = target.GetChildren(); + Assert.Equal(2, children.Count); + } + } + + public class Parent + { + [Fact] + public void Connects_Peer_To_Tree_When_GetParent_Called() + { + var border = new Border(); + var tree = new Decorator + { + Child = new Decorator + { + Child = border, + } + }; + + var factory = CreateFactory(); + + // We're accessing Border directly without going via its ancestors. Because the tree + // is built lazily, ensure that calling GetParent causes the ancestor tree to be built. + var target = CreatePeer(factory, border); + + var parentPeer = Assert.IsAssignableFrom(target.GetParent()); + Assert.Same(border.GetVisualParent(), parentPeer.Owner); + } + + [Fact] + public void Parent_Updated_When_Moved_To_Separate_Visual_Tree() + { + var border = new Border(); + var root1 = new Decorator { Child = border }; + var root2 = new Decorator(); + var factory = CreateFactory(); + var target = CreatePeer(factory, border); + + var parentPeer = Assert.IsAssignableFrom(target.GetParent()); + Assert.Same(root1, parentPeer.Owner); + + root1.Child = null; + + Assert.Null(target.GetParent()); + + root2.Child = border; + + parentPeer = Assert.IsAssignableFrom(target.GetParent()); + Assert.Same(root2, parentPeer.Owner); + } + } + + private static IAutomationNodeFactory CreateFactory() + { + var factory = new Mock(); + factory.Setup(x => x.CreateNode(It.IsAny())) + .Returns(() => Mock.Of(x => x.Factory == factory.Object)); + return factory.Object; + } + + private static AutomationPeer CreatePeer(IAutomationNodeFactory factory, Control control) + { + return ControlAutomationPeer.GetOrCreatePeer(factory, control); + } + + private class TestControl : Control + { + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new TestAutomationPeer(factory, this); + } + } + + private class AutomationTestRoot : TestRoot + { + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new TestRootAutomationPeer(factory, this); + } + } + + private class TestAutomationPeer : ControlAutomationPeer + { + public TestAutomationPeer(IAutomationNodeFactory factory, Control owner) + : base(factory, owner) + { + } + } + + private class TestRootAutomationPeer : ControlAutomationPeer, IRootProvider + { + public TestRootAutomationPeer(IAutomationNodeFactory factory, Control owner) + : base(factory, owner) + { + } + + public ITopLevelImpl PlatformImpl => throw new System.NotImplementedException(); + + public AutomationPeer GetFocus() + { + throw new System.NotImplementedException(); + } + + public AutomationPeer GetPeerFromPoint(Point p) + { + throw new System.NotImplementedException(); + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 9223b08c817..697179af744 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -491,6 +491,7 @@ public void Closing_Popup_Sets_Focus_On_PlacementTarget() using (CreateServicesWithFocus()) { var window = PreparedWindow(); + window.Focusable = true; var tb = new TextBox(); var p = new Popup diff --git a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs index df0a077c7f4..382bd762d59 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs @@ -86,5 +86,30 @@ public void TextInput_Should_Be_Sent_To_Focused_Element() focused.Verify(x => x.RaiseEvent(It.IsAny())); } + + [Fact] + public void Control_Focus_Should_Be_Set_Before_FocusedElement_Raises_PropertyChanged() + { + var target = new KeyboardDevice(); + var focused = new Mock(); + var root = Mock.Of(); + var raised = 0; + + target.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(target.FocusedElement)) + { + focused.Verify(x => x.RaiseEvent(It.IsAny())); + ++raised; + } + }; + + target.SetFocusedElement( + focused.Object, + NavigationMethod.Unspecified, + KeyModifiers.None); + + Assert.Equal(1, raised); + } } } diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 8a24a8366f5..983f48e5f0c 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -85,7 +85,12 @@ public static Mock CreatePopupMock(IWindowBaseImpl parent) popupImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize); popupImpl.Setup(x => x.RenderScaling).Returns(1); popupImpl.Setup(x => x.PopupPositioner).Returns(positioner); - + + popupImpl.Setup(x => x.Dispose()).Callback(() => + { + popupImpl.Object.Closed?.Invoke(); + }); + SetupToplevel(popupImpl); return popupImpl; From c1dfc1f9634e1004486d2c3f9ba0eace3a248a4a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 9 Mar 2021 16:40:08 +0100 Subject: [PATCH 02/73] Started adding integration tests. --- ...a.IntegrationTests.Win32.v3.ncrunchproject | 5 ++ .ncrunch/IntegrationTestApp.v3.ncrunchproject | 5 ++ Avalonia.sln | 60 ++++++++++++++++++- samples/IntegrationTestApp/App.axaml | 7 +++ samples/IntegrationTestApp/App.axaml.cs | 24 ++++++++ .../IntegrationTestApp.csproj | 10 ++++ samples/IntegrationTestApp/MainWindow.axaml | 22 +++++++ .../IntegrationTestApp/MainWindow.axaml.cs | 19 ++++++ samples/IntegrationTestApp/Program.cs | 22 +++++++ samples/IntegrationTestApp/nuget.config | 11 ++++ .../Automation/Peers/ButtonAutomationPeer.cs | 3 + .../Properties/AssemblyInfo.cs | 1 + .../Avalonia.IntegrationTests.Win32.csproj | 13 ++++ .../ButtonTests.cs | 40 +++++++++++++ .../Properties/AssemblyInfo.cs | 4 ++ .../TestAppCollection.cs | 9 +++ .../TestAppFixture.cs | 28 +++++++++ .../xunit.runner.json | 3 + 18 files changed, 283 insertions(+), 3 deletions(-) create mode 100644 .ncrunch/Avalonia.IntegrationTests.Win32.v3.ncrunchproject create mode 100644 .ncrunch/IntegrationTestApp.v3.ncrunchproject create mode 100644 samples/IntegrationTestApp/App.axaml create mode 100644 samples/IntegrationTestApp/App.axaml.cs create mode 100644 samples/IntegrationTestApp/IntegrationTestApp.csproj create mode 100644 samples/IntegrationTestApp/MainWindow.axaml create mode 100644 samples/IntegrationTestApp/MainWindow.axaml.cs create mode 100644 samples/IntegrationTestApp/Program.cs create mode 100644 samples/IntegrationTestApp/nuget.config create mode 100644 tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj create mode 100644 tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs create mode 100644 tests/Avalonia.IntegrationTests.Win32/Properties/AssemblyInfo.cs create mode 100644 tests/Avalonia.IntegrationTests.Win32/TestAppCollection.cs create mode 100644 tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs create mode 100644 tests/Avalonia.IntegrationTests.Win32/xunit.runner.json diff --git a/.ncrunch/Avalonia.IntegrationTests.Win32.v3.ncrunchproject b/.ncrunch/Avalonia.IntegrationTests.Win32.v3.ncrunchproject new file mode 100644 index 00000000000..319cd523cec --- /dev/null +++ b/.ncrunch/Avalonia.IntegrationTests.Win32.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/IntegrationTestApp.v3.ncrunchproject b/.ncrunch/IntegrationTestApp.v3.ncrunchproject new file mode 100644 index 00000000000..319cd523cec --- /dev/null +++ b/.ncrunch/IntegrationTestApp.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/Avalonia.sln b/Avalonia.sln index 75f1dd84078..96d35e1bb9b 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -226,11 +226,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "samples\Sandbox\Sandbox.csproj", "{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroComGenerator", "src\tools\MicroComGenerator\MicroComGenerator.csproj", "{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroComGenerator", "src\tools\MicroComGenerator\MicroComGenerator.csproj", "{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.MicroCom", "src\Avalonia.MicroCom\Avalonia.MicroCom.csproj", "{FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.MicroCom", "src\Avalonia.MicroCom\Avalonia.MicroCom.csproj", "{FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniMvvm", "samples\MiniMvvm\MiniMvvm.csproj", "{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniMvvm", "samples\MiniMvvm\MiniMvvm.csproj", "{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestApp", "samples\IntegrationTestApp\IntegrationTestApp.csproj", "{676D6BFD-029D-4E43-BFC7-3892265CE251}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.IntegrationTests.Win32", "tests\Avalonia.IntegrationTests.Win32\Avalonia.IntegrationTests.Win32.csproj", "{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -2142,6 +2146,54 @@ Global {BC594FD5-4AF2-409E-A1E6-04123F54D7C5}.Release|iPhone.Build.0 = Release|Any CPU {BC594FD5-4AF2-409E-A1E6-04123F54D7C5}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {BC594FD5-4AF2-409E-A1E6-04123F54D7C5}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.AppStore|iPhone.Build.0 = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|Any CPU.Build.0 = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|iPhone.Build.0 = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|Any CPU.ActiveCfg = Release|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|Any CPU.Build.0 = Release|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|iPhone.ActiveCfg = Release|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|iPhone.Build.0 = Release|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.AppStore|iPhone.Build.0 = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|iPhone.Build.0 = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|Any CPU.Build.0 = Release|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|iPhone.ActiveCfg = Release|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|iPhone.Build.0 = Release|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2203,6 +2255,8 @@ Global {11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098} {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {BC594FD5-4AF2-409E-A1E6-04123F54D7C5} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {676D6BFD-029D-4E43-BFC7-3892265CE251} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/samples/IntegrationTestApp/App.axaml b/samples/IntegrationTestApp/App.axaml new file mode 100644 index 00000000000..a833e096dfe --- /dev/null +++ b/samples/IntegrationTestApp/App.axaml @@ -0,0 +1,7 @@ + + + + + diff --git a/samples/IntegrationTestApp/App.axaml.cs b/samples/IntegrationTestApp/App.axaml.cs new file mode 100644 index 00000000000..022931366d3 --- /dev/null +++ b/samples/IntegrationTestApp/App.axaml.cs @@ -0,0 +1,24 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace IntegrationTestApp +{ + public class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } + } +} diff --git a/samples/IntegrationTestApp/IntegrationTestApp.csproj b/samples/IntegrationTestApp/IntegrationTestApp.csproj new file mode 100644 index 00000000000..89e793837ab --- /dev/null +++ b/samples/IntegrationTestApp/IntegrationTestApp.csproj @@ -0,0 +1,10 @@ + + + WinExe + netcoreapp3.1 + enable + + + + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml new file mode 100644 index 00000000000..5daa7c5c18d --- /dev/null +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs new file mode 100644 index 00000000000..5a14d67aa5d --- /dev/null +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace IntegrationTestApp +{ + public class MainWindow : Window + { + public MainWindow() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/IntegrationTestApp/Program.cs b/samples/IntegrationTestApp/Program.cs new file mode 100644 index 00000000000..c09b249cfae --- /dev/null +++ b/samples/IntegrationTestApp/Program.cs @@ -0,0 +1,22 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; + +namespace IntegrationTestApp +{ + class Program + { + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); + } +} diff --git a/samples/IntegrationTestApp/nuget.config b/samples/IntegrationTestApp/nuget.config new file mode 100644 index 00000000000..6c273ab3d9b --- /dev/null +++ b/samples/IntegrationTestApp/nuget.config @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs index ded27f14bdc..89c80e1144c 100644 --- a/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs @@ -24,6 +24,9 @@ protected override AutomationControlType GetAutomationControlTypeCore() { return AutomationControlType.Button; } + + protected override bool IsContentElementCore() => true; + protected override bool IsControlElementCore() => true; } } diff --git a/src/Avalonia.Controls/Properties/AssemblyInfo.cs b/src/Avalonia.Controls/Properties/AssemblyInfo.cs index 5c9017f1272..5ccafb725bc 100644 --- a/src/Avalonia.Controls/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Controls/Properties/AssemblyInfo.cs @@ -9,6 +9,7 @@ [assembly: InternalsVisibleTo("Avalonia.DesignerSupport")] #endif [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Automation")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Embedding")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Presenters")] diff --git a/tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj b/tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj new file mode 100644 index 00000000000..0967944e197 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp3.1 + enable + + + + + + + + diff --git a/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs b/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs new file mode 100644 index 00000000000..03a156a9e25 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs @@ -0,0 +1,40 @@ +using OpenQA.Selenium.Appium.Windows; +using Xunit; + +namespace Avalonia.IntegrationTests.Win32 +{ + [Collection("IntegrationTestApp collection")] + public class ButtonTests + { + private WindowsDriver _session; + public ButtonTests(TestAppFixture fixture) => _session = fixture.Session; + + [Fact] + public void BasicButton() + { + SelectButtonTab(); + + var button = _session.FindElementByAccessibilityId("BasicButton"); + + Assert.Equal("Basic Button", button.Text); + } + + [Fact] + public void ButtonWithTextBlock() + { + SelectButtonTab(); + + var button = _session.FindElementByAccessibilityId("ButtonWithTextBlock"); + + Assert.Equal("Button with TextBlock", button.Text); + } + + private WindowsElement SelectButtonTab() + { + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var buttonTab = tabs.FindElementByName("Button"); + buttonTab.Click(); + return (WindowsElement)buttonTab; + } + } +} diff --git a/tests/Avalonia.IntegrationTests.Win32/Properties/AssemblyInfo.cs b/tests/Avalonia.IntegrationTests.Win32/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..f9248a31524 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Win32/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using Xunit; + +// Don't run tests in parallel. +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/tests/Avalonia.IntegrationTests.Win32/TestAppCollection.cs b/tests/Avalonia.IntegrationTests.Win32/TestAppCollection.cs new file mode 100644 index 00000000000..6461e14596a --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Win32/TestAppCollection.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace Avalonia.IntegrationTests.Win32 +{ + [CollectionDefinition("IntegrationTestApp collection")] + public class TestAppCollection : ICollectionFixture + { + } +} diff --git a/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs b/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs new file mode 100644 index 00000000000..6337ff1c6f7 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs @@ -0,0 +1,28 @@ +using System; +using System.IO; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Windows; + +namespace Avalonia.IntegrationTests.Win32 +{ + public class TestAppFixture : IDisposable + { + private const string WindowsApplicationDriverUrl = "http://127.0.0.1:4723"; + private const string TestAppPath = @"..\..\..\..\..\samples\IntegrationTestApp\bin\Debug\netcoreapp3.1\IntegrationTestApp.exe"; + + public TestAppFixture() + { + var opts = new AppiumOptions(); + var path = Path.GetFullPath(TestAppPath); + opts.AddAdditionalCapability("app", path); + opts.AddAdditionalCapability("deviceName", "WindowsPC"); + Session = new WindowsDriver( + new Uri(WindowsApplicationDriverUrl), + opts); + } + + public WindowsDriver Session { get; } + + public void Dispose() => Session.Close(); + } +} diff --git a/tests/Avalonia.IntegrationTests.Win32/xunit.runner.json b/tests/Avalonia.IntegrationTests.Win32/xunit.runner.json new file mode 100644 index 00000000000..f78bc2f0c65 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Win32/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json" +} \ No newline at end of file From e8801c2acaba1e1bd171d717747f286962f5693d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 9 Mar 2021 22:46:12 +0100 Subject: [PATCH 03/73] Started adding ComboBox integration tests. And fixed some issues with ComboBox/popups. --- .../IntegrationTestApp.csproj | 3 + samples/IntegrationTestApp/MainWindow.axaml | 17 ++++++ .../IntegrationTestApp/MainWindow.axaml.cs | 1 + .../Peers/ListItemAutomationPeer.cs | 3 + .../Automation/Peers/PopupAutomationPeer.cs | 52 ++++++++++++++++++ .../Peers/PopupRootAutomationPeer.cs | 7 +++ src/Avalonia.Controls/Primitives/Popup.cs | 5 ++ .../Automation/AutomationNode.cs | 11 +++- .../ButtonTests.cs | 24 ++++++-- .../ComboBoxTests.cs | 55 +++++++++++++++++++ 10 files changed, 169 insertions(+), 9 deletions(-) create mode 100644 src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs create mode 100644 tests/Avalonia.IntegrationTests.Win32/ComboBoxTests.cs diff --git a/samples/IntegrationTestApp/IntegrationTestApp.csproj b/samples/IntegrationTestApp/IntegrationTestApp.csproj index 89e793837ab..423b4281817 100644 --- a/samples/IntegrationTestApp/IntegrationTestApp.csproj +++ b/samples/IntegrationTestApp/IntegrationTestApp.csproj @@ -7,4 +7,7 @@ + + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 5daa7c5c18d..f70e3ab0400 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -8,6 +8,9 @@ + @@ -17,6 +20,20 @@ + + + Foo + Bar + + + Foo + Bar + + + Foo + Bar + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 5a14d67aa5d..1d3ca28432c 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -9,6 +9,7 @@ public class MainWindow : Window public MainWindow() { InitializeComponent(); + this.AttachDevTools(); } private void InitializeComponent() diff --git a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs index a9e5089e434..1b9a8354a1b 100644 --- a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs @@ -78,5 +78,8 @@ protected override AutomationControlType GetAutomationControlTypeCore() { return AutomationControlType.ListItem; } + + protected override bool IsContentElementCore() => true; + protected override bool IsControlElementCore() => true; } } diff --git a/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs new file mode 100644 index 00000000000..4bad8fd1083 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using Avalonia.Automation.Platform; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.VisualTree; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class PopupAutomationPeer : ControlAutomationPeer + { + public PopupAutomationPeer(IAutomationNodeFactory factory, Popup owner) + : base(factory, owner) + { + owner.Opened += PopupOpenedClosed; + owner.Closed += PopupOpenedClosed; + } + + protected override IReadOnlyList? GetChildrenCore() + { + var host = (IVisualTreeHost)Owner; + System.Diagnostics.Debug.WriteLine($"Popup children='{host}'"); + return host.Root is Control c ? new[] { GetOrCreatePeer(c) } : null; + } + + protected override bool IsContentElementCore() => false; + protected override bool IsControlElementCore() => false; + + private void PopupOpenedClosed(object sender, EventArgs e) + { + // This is golden. We're following WPF's automation peer API here where the + // parent of a peer is set when another peer returns it as a child. We want to + // add the popup root as a child of the popup, so we need to return it as a + // child right? Yeah except invalidating children doesn't automatically cause + // UIA to re-read the children meaning that the parent doesn't get set. So the + // MAIN MECHANISM FOR PARENTING CONTROLS IS BROKEN WITH THE ONLY AUTOMATION API + // IT WAS WRITTEN FOR. Luckily WPF provides an escape-hatch by exposing the + // TrySetParent API internally to work around this. We're exposing it publicly + // to shame whoever came up with this abomination of an API. + GetPopupRoot()?.TrySetParent(this); + InvalidateChildren(); + } + + private AutomationPeer? GetPopupRoot() + { + var popupRoot = ((IVisualTreeHost)Owner).Root as Control; + return popupRoot is object ? GetOrCreatePeer(popupRoot) : null; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs index ed0e8f6d440..b54a938a7f0 100644 --- a/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs @@ -21,6 +21,13 @@ public PopupRootAutomationPeer(IAutomationNodeFactory factory, PopupRoot owner) protected override bool IsContentElementCore() => false; protected override bool IsControlElementCore() => false; + + protected override AutomationPeer? GetParentCore() + { + var parent = base.GetParentCore(); + return parent; + } + private void OnOpened(object sender, EventArgs e) { ((PopupRoot)Owner).Opened -= OnOpened; diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 1837b90c5dd..0aa27dd1006 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -516,6 +516,11 @@ protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs Close(); } + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new PopupAutomationPeer(factory, this); + } + private static IDisposable SubscribeToEventHandler(T target, TEventHandler handler, Action subscribe, Action unsubscribe) { subscribe(target, handler); diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index ca237567c1b..3757958cca1 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -82,8 +82,8 @@ public void ChildrenChanged() UiaCoreProviderApi.UiaRaiseStructureChangedEvent( this, StructureChangeType.ChildrenInvalidated, - _runtimeId, - _runtimeId.Length); + null, + 0); } public void PropertyChanged(AutomationProperty property, object? oldValue, object? newValue) @@ -156,6 +156,11 @@ public void PropertyChanged(AutomationProperty property, object? oldValue, objec return null; } + if (Peer.GetType().Name == "PopupAutomationPeer") + { + System.Diagnostics.Debug.WriteLine("Popup automation node navigate " + direction); + } + return InvokeSync(() => { return direction switch @@ -269,7 +274,7 @@ private AutomationPeer GetRoot() var peer = Peer; var parent = peer.GetParent(); - while (parent is object) + while (peer is not AAP.IRootProvider && parent is object) { peer = parent; parent = peer.GetParent(); diff --git a/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs b/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs index 03a156a9e25..e8548430f3f 100644 --- a/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs +++ b/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs @@ -9,32 +9,44 @@ public class ButtonTests private WindowsDriver _session; public ButtonTests(TestAppFixture fixture) => _session = fixture.Session; + [Fact] + public void DisabledButton() + { + SelectTab(); + + var button = _session.FindElementByAccessibilityId("DisabledButton"); + + Assert.Equal("Disabled Button", button.Text); + Assert.False(button.Enabled); + } + [Fact] public void BasicButton() { - SelectButtonTab(); + SelectTab(); var button = _session.FindElementByAccessibilityId("BasicButton"); Assert.Equal("Basic Button", button.Text); + Assert.True(button.Enabled); } [Fact] public void ButtonWithTextBlock() { - SelectButtonTab(); + SelectTab(); var button = _session.FindElementByAccessibilityId("ButtonWithTextBlock"); Assert.Equal("Button with TextBlock", button.Text); } - private WindowsElement SelectButtonTab() + private WindowsElement SelectTab() { var tabs = _session.FindElementByAccessibilityId("MainTabs"); - var buttonTab = tabs.FindElementByName("Button"); - buttonTab.Click(); - return (WindowsElement)buttonTab; + var tab = tabs.FindElementByName("Button"); + tab.Click(); + return (WindowsElement)tab; } } } diff --git a/tests/Avalonia.IntegrationTests.Win32/ComboBoxTests.cs b/tests/Avalonia.IntegrationTests.Win32/ComboBoxTests.cs new file mode 100644 index 00000000000..95ca1ed14aa --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Win32/ComboBoxTests.cs @@ -0,0 +1,55 @@ +using OpenQA.Selenium.Appium.Windows; +using Xunit; + +namespace Avalonia.IntegrationTests.Win32 +{ + [Collection("IntegrationTestApp collection")] + public class ComboBoxTests + { + private WindowsDriver _session; + public ComboBoxTests(TestAppFixture fixture) => _session = fixture.Session; + + [Fact] + public void UnselectedComboBox() + { + SelectTab(); + + var comboBox = _session.FindElementByAccessibilityId("UnselectedComboBox"); + + Assert.Equal(string.Empty, comboBox.Text); + + comboBox.Click(); + comboBox.FindElementByName("Bar").Click(); + + Assert.Equal("Bar", comboBox.Text); + } + + [Fact] + public void SelectedIndex0ComboBox() + { + SelectTab(); + + var comboBox = _session.FindElementByAccessibilityId("SelectedIndex0ComboBox"); + + Assert.Equal("Foo", comboBox.Text); + } + + [Fact] + public void SelectedIndex1ComboBox() + { + SelectTab(); + + var comboBox = _session.FindElementByAccessibilityId("SelectedIndex1ComboBox"); + + Assert.Equal("Bar", comboBox.Text); + } + + private WindowsElement SelectTab() + { + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("ComboBox"); + tab.Click(); + return (WindowsElement)tab; + } + } +} From 5f551a2dd9f178b5272c54d0b29af18d55a43f77 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 9 Mar 2021 23:07:34 +0100 Subject: [PATCH 04/73] Make integration tests ordered. --- build/XUnit.props | 18 ++++++------- .../Avalonia.IntegrationTests.Win32.csproj | 1 + .../ButtonTests.cs | 26 +++++++------------ .../ComboBoxTests.cs | 26 +++++++------------ .../DefaultCollection.cs | 9 +++++++ .../TestAppCollection.cs | 9 ------- 6 files changed, 39 insertions(+), 50 deletions(-) create mode 100644 tests/Avalonia.IntegrationTests.Win32/DefaultCollection.cs delete mode 100644 tests/Avalonia.IntegrationTests.Win32/TestAppCollection.cs diff --git a/build/XUnit.props b/build/XUnit.props index a75e1bac863..17ead91aa3c 100644 --- a/build/XUnit.props +++ b/build/XUnit.props @@ -1,14 +1,14 @@  - - - - - - - - - + + + + + + + + + diff --git a/tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj b/tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj index 0967944e197..f38f8b0ce16 100644 --- a/tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj +++ b/tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj @@ -7,6 +7,7 @@ + diff --git a/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs b/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs index e8548430f3f..b0d300e9fe0 100644 --- a/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs +++ b/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs @@ -3,17 +3,23 @@ namespace Avalonia.IntegrationTests.Win32 { - [Collection("IntegrationTestApp collection")] + [Collection("Default")] public class ButtonTests { private WindowsDriver _session; - public ButtonTests(TestAppFixture fixture) => _session = fixture.Session; + + public ButtonTests(TestAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("Button"); + tab.Click(); + } [Fact] public void DisabledButton() { - SelectTab(); - var button = _session.FindElementByAccessibilityId("DisabledButton"); Assert.Equal("Disabled Button", button.Text); @@ -23,8 +29,6 @@ public void DisabledButton() [Fact] public void BasicButton() { - SelectTab(); - var button = _session.FindElementByAccessibilityId("BasicButton"); Assert.Equal("Basic Button", button.Text); @@ -34,19 +38,9 @@ public void BasicButton() [Fact] public void ButtonWithTextBlock() { - SelectTab(); - var button = _session.FindElementByAccessibilityId("ButtonWithTextBlock"); Assert.Equal("Button with TextBlock", button.Text); } - - private WindowsElement SelectTab() - { - var tabs = _session.FindElementByAccessibilityId("MainTabs"); - var tab = tabs.FindElementByName("Button"); - tab.Click(); - return (WindowsElement)tab; - } } } diff --git a/tests/Avalonia.IntegrationTests.Win32/ComboBoxTests.cs b/tests/Avalonia.IntegrationTests.Win32/ComboBoxTests.cs index 95ca1ed14aa..6764343c02f 100644 --- a/tests/Avalonia.IntegrationTests.Win32/ComboBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Win32/ComboBoxTests.cs @@ -3,17 +3,23 @@ namespace Avalonia.IntegrationTests.Win32 { - [Collection("IntegrationTestApp collection")] + [Collection("Default")] public class ComboBoxTests { private WindowsDriver _session; - public ComboBoxTests(TestAppFixture fixture) => _session = fixture.Session; + + public ComboBoxTests(TestAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("ComboBox"); + tab.Click(); + } [Fact] public void UnselectedComboBox() { - SelectTab(); - var comboBox = _session.FindElementByAccessibilityId("UnselectedComboBox"); Assert.Equal(string.Empty, comboBox.Text); @@ -27,8 +33,6 @@ public void UnselectedComboBox() [Fact] public void SelectedIndex0ComboBox() { - SelectTab(); - var comboBox = _session.FindElementByAccessibilityId("SelectedIndex0ComboBox"); Assert.Equal("Foo", comboBox.Text); @@ -37,19 +41,9 @@ public void SelectedIndex0ComboBox() [Fact] public void SelectedIndex1ComboBox() { - SelectTab(); - var comboBox = _session.FindElementByAccessibilityId("SelectedIndex1ComboBox"); Assert.Equal("Bar", comboBox.Text); } - - private WindowsElement SelectTab() - { - var tabs = _session.FindElementByAccessibilityId("MainTabs"); - var tab = tabs.FindElementByName("ComboBox"); - tab.Click(); - return (WindowsElement)tab; - } } } diff --git a/tests/Avalonia.IntegrationTests.Win32/DefaultCollection.cs b/tests/Avalonia.IntegrationTests.Win32/DefaultCollection.cs new file mode 100644 index 00000000000..1c8f09a430d --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Win32/DefaultCollection.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace Avalonia.IntegrationTests.Win32 +{ + [CollectionDefinition("Default")] + public class DefaultCollection : ICollectionFixture + { + } +} diff --git a/tests/Avalonia.IntegrationTests.Win32/TestAppCollection.cs b/tests/Avalonia.IntegrationTests.Win32/TestAppCollection.cs deleted file mode 100644 index 6461e14596a..00000000000 --- a/tests/Avalonia.IntegrationTests.Win32/TestAppCollection.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Xunit; - -namespace Avalonia.IntegrationTests.Win32 -{ - [CollectionDefinition("IntegrationTestApp collection")] - public class TestAppCollection : ICollectionFixture - { - } -} From 2e2333399690d41797624631e9f781d01b9f1cc2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 10 Mar 2021 22:37:08 +0100 Subject: [PATCH 05/73] Add CheckBox tests. --- .editorconfig | 2 +- Avalonia.sln | 3 +- samples/IntegrationTestApp/MainWindow.axaml | 43 ++++++----- .../Peers/ToggleButtonAutomationPeer.cs | 3 + .../CheckBoxTests.cs | 72 +++++++++++++++++++ .../TestAppFixture.cs | 11 +++ 6 files changed, 115 insertions(+), 19 deletions(-) create mode 100644 tests/Avalonia.IntegrationTests.Win32/CheckBoxTests.cs diff --git a/.editorconfig b/.editorconfig index c7a381b730b..25e0135725a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -137,7 +137,7 @@ space_within_single_line_array_initializer_braces = true csharp_wrap_before_ternary_opsigns = false # Xaml files -[*.xaml] +[*.{xaml,axaml}] indent_size = 2 # Xml project files diff --git a/Avalonia.sln b/Avalonia.sln index 96d35e1bb9b..0dae88db96a 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -234,7 +234,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniMvvm", "samples\MiniMvv EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestApp", "samples\IntegrationTestApp\IntegrationTestApp.csproj", "{676D6BFD-029D-4E43-BFC7-3892265CE251}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.IntegrationTests.Win32", "tests\Avalonia.IntegrationTests.Win32\Avalonia.IntegrationTests.Win32.csproj", "{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.IntegrationTests.Win32", "tests\Avalonia.IntegrationTests.Win32\Avalonia.IntegrationTests.Win32.csproj", "{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -2235,6 +2235,7 @@ Global {29132311-1848-4FD6-AE0C-4FF841151BD3} = {9B9E3891-2366-4253-A952-D08BCEB71098} {7D2D3083-71DD-4CC9-8907-39A0D86FB322} = {3743B0F2-CC41-4F14-A8C8-267F579BF91E} {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} = {A689DEF5-D50F-4975-8B72-124C9EB54066} {854568D5-13D1-4B4F-B50D-534DC7EFD3C9} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {638580B0-7910-40EF-B674-DCB34DA308CD} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E} = {B39A8919-9F95-48FE-AD7B-76E08B509888} diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index f70e3ab0400..098b4dc3aab 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -9,31 +9,40 @@ + + + + Unchecked + Checked + ThreeState + + + - - - Foo - Bar - - - Foo - Bar - - - Foo - Bar - - + + + Foo + Bar + + + Foo + Bar + + + Foo + Bar + + diff --git a/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs index b103df31653..4c410d8654d 100644 --- a/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs @@ -35,5 +35,8 @@ protected override AutomationControlType GetAutomationControlTypeCore() { return AutomationControlType.Button; } + + protected override bool IsContentElementCore() => true; + protected override bool IsControlElementCore() => true; } } diff --git a/tests/Avalonia.IntegrationTests.Win32/CheckBoxTests.cs b/tests/Avalonia.IntegrationTests.Win32/CheckBoxTests.cs new file mode 100644 index 00000000000..ebf7408eab4 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Win32/CheckBoxTests.cs @@ -0,0 +1,72 @@ +using OpenQA.Selenium.Appium.Windows; +using Xunit; + +namespace Avalonia.IntegrationTests.Win32 +{ + [Collection("Default")] + public class CheckBoxTests + { + private WindowsDriver _session; + + public CheckBoxTests(TestAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("CheckBox"); + tab.Click(); + } + + [Fact] + public void UncheckedCheckBox() + { + var checkBox = _session.FindElementByAccessibilityId("UncheckedCheckBox"); + + Assert.Equal("Unchecked", checkBox.Text); + Assert.False(checkBox.Selected); + Assert.Equal("0", checkBox.GetAttribute("Toggle.ToggleState")); + + checkBox.Click(); + + Assert.True(checkBox.Selected); + Assert.Equal("1", checkBox.GetAttribute("Toggle.ToggleState")); + } + + [Fact] + public void CheckedCheckBox() + { + var checkBox = _session.FindElementByAccessibilityId("CheckedCheckBox"); + + Assert.Equal("Checked", checkBox.Text); + Assert.True(checkBox.Selected); + Assert.Equal("1", checkBox.GetAttribute("Toggle.ToggleState")); + + checkBox.Click(); + + Assert.False(checkBox.Selected); + Assert.Equal("0", checkBox.GetAttribute("Toggle.ToggleState")); + } + + [Fact] + public void ThreeStateCheckBox() + { + var checkBox = _session.FindElementByAccessibilityId("ThreeStateCheckBox"); + + Assert.Equal("ThreeState", checkBox.Text); + Assert.Equal("2", checkBox.GetAttribute("Toggle.ToggleState")); + + checkBox.Click(); + + Assert.False(checkBox.Selected); + Assert.Equal("0", checkBox.GetAttribute("Toggle.ToggleState")); + + checkBox.Click(); + + Assert.True(checkBox.Selected); + Assert.Equal("1", checkBox.GetAttribute("Toggle.ToggleState")); + + checkBox.Click(); + Assert.Equal("2", checkBox.GetAttribute("Toggle.ToggleState")); + } + } +} diff --git a/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs b/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs index 6337ff1c6f7..fe2daa3cd0a 100644 --- a/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs +++ b/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs @@ -1,5 +1,7 @@ using System; +using System.Globalization; using System.IO; +using System.Runtime.InteropServices; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Windows; @@ -19,10 +21,19 @@ public TestAppFixture() Session = new WindowsDriver( new Uri(WindowsApplicationDriverUrl), opts); + + // https://github.com/microsoft/WinAppDriver/issues/1025 + SetForegroundWindow(new IntPtr(int.Parse( + Session.WindowHandles[0].Substring(2), + NumberStyles.AllowHexSpecifier))); } public WindowsDriver Session { get; } public void Dispose() => Session.Close(); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetForegroundWindow(IntPtr hWnd); } } From 651106aed34ee8c610bbc40f6752fdc88eac6e96 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 Mar 2021 15:29:41 +0100 Subject: [PATCH 06/73] Use Name for AutomationId. --- samples/IntegrationTestApp/MainWindow.axaml | 30 ++++++++++++------- .../Automation/Peers/ControlAutomationPeer.cs | 2 +- .../AutomationTests.cs | 29 ++++++++++++++++++ 3 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 098b4dc3aab..bd9f2a0d320 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -5,16 +5,24 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="IntegrationTestApp.MainWindow" Title="IntegrationTestApp"> - + + + + TextBlockWithName + + TextBlockWithNameAndAutomationId + + + - - - @@ -22,23 +30,23 @@ - Unchecked - Checked - ThreeState + Unchecked + Checked + ThreeState - + - + Foo Bar - + Foo Bar - + Foo Bar diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index 79899866530..a6e7a920e30 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -43,7 +43,7 @@ public AutomationPeer GetOrCreatePeer(Control element) } protected override void BringIntoViewCore() => Owner.BringIntoView(); - protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner); + protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner) ?? Owner.Name; protected override Rect GetBoundingRectangleCore() => GetBounds(Owner.TransformedBounds); protected virtual IReadOnlyList? GetChildrenCore() diff --git a/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs b/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs new file mode 100644 index 00000000000..3c6d9172c43 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs @@ -0,0 +1,29 @@ +using OpenQA.Selenium.Appium.Windows; +using Xunit; + +namespace Avalonia.IntegrationTests.Win32 +{ + [Collection("Default")] + public class AutomationTests + { + private WindowsDriver _session; + + public AutomationTests(TestAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("Automation"); + tab.Click(); + } + + [Fact] + public void AutomationId() + { + // AutomationID can be specified by the Name or AutomationProperties.AutomationId + // properties, with the latter taking precedence. + var byName = _session.FindElementByAccessibilityId("TextBlockWithName"); + var byAutomationId = _session.FindElementByAccessibilityId("TextBlockWithNameAndAutomationId"); + } + } +} From 8b642a91417a6a8862d6ea0500f298930c0798c7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 Mar 2021 16:28:48 +0100 Subject: [PATCH 07/73] Implemented LabeledBy. --- samples/IntegrationTestApp/MainWindow.axaml | 4 + .../Automation/Peers/AutomationPeer.cs | 7 ++ .../Peers/ComboBoxAutomationPeer.cs | 1 + .../Automation/Peers/ControlAutomationPeer.cs | 77 +++++++++++-------- ...tionPeer.cs => TextBlockAutomationPeer.cs} | 8 +- .../Automation/Peers/TextBoxAutomationPeer.cs | 7 +- src/Avalonia.Controls/TextBlock.cs | 2 +- .../AutomationTests.cs | 10 +++ 8 files changed, 81 insertions(+), 35 deletions(-) rename src/Avalonia.Controls/Automation/Peers/{TextAutomationPeer.cs => TextBlockAutomationPeer.cs} (67%) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index bd9f2a0d320..0ccca5d4cc0 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -12,6 +12,10 @@ TextBlockWithNameAndAutomationId + Label for TextBox + + Foo + diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index aefd1febd06..8a306613591 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -101,6 +101,12 @@ protected AutomationPeer(IAutomationNodeFactory factory) /// public string GetClassName() => GetClassNameCore() ?? string.Empty; + /// + /// Gets the automation peer for the label that is targeted to the element. + /// + /// + public AutomationPeer? GetLabeledBy() => GetLabeledByCore(); + /// /// Gets a human-readable localized string that represents the type of the control that is /// associated with this automation peer. @@ -207,6 +213,7 @@ protected virtual string GetLocalizedControlTypeCore() protected abstract Rect GetBoundingRectangleCore(); protected abstract IReadOnlyList GetOrCreateChildrenCore(); protected abstract string GetClassNameCore(); + protected abstract AutomationPeer? GetLabeledByCore(); protected abstract string? GetNameCore(); protected abstract AutomationPeer? GetParentCore(); protected abstract bool HasKeyboardFocusCore(); diff --git a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs index 698bfd14607..721272c83ec 100644 --- a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs @@ -94,6 +94,7 @@ public object? Item protected override string? GetAutomationIdCore() => null; protected override string GetClassNameCore() => typeof(ComboBoxItem).Name; + protected override AutomationPeer? GetLabeledByCore() => null; protected override AutomationPeer? GetParentCore() => _owner; protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.ListItem; diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index a6e7a920e30..e68bf2ea471 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -43,8 +43,24 @@ public AutomationPeer GetOrCreatePeer(Control element) } protected override void BringIntoViewCore() => Owner.BringIntoView(); - protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner) ?? Owner.Name; - protected override Rect GetBoundingRectangleCore() => GetBounds(Owner.TransformedBounds); + + 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() { @@ -66,6 +82,24 @@ public AutomationPeer GetOrCreatePeer(Control element) return result; } + protected override AutomationPeer? GetLabeledByCore() + { + var label = AutomationProperties.GetLabeledBy(Owner); + return label is Control c ? GetOrCreatePeer(c) : null; + } + + protected override string? GetNameCore() + { + var result = AutomationProperties.GetName(Owner); + + if (string.IsNullOrWhiteSpace(result) && GetLabeledBy() is AutomationPeer labeledBy) + { + return labeledBy.GetName(); + } + + return null; + } + protected override AutomationPeer? GetParentCore() { EnsureConnected(); @@ -90,34 +124,6 @@ protected void InvalidateParent() _parentValid = false; } - 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 override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Custom; - protected override string GetClassNameCore() => Owner.GetType().Name; - protected override string? GetNameCore() => AutomationProperties.GetName(Owner); - protected override bool HasKeyboardFocusCore() => Owner.IsFocused; - protected override bool IsContentElementCore() => true; - protected override bool IsControlElementCore() => true; - protected override bool IsEnabledCore() => Owner.IsEnabled; - protected override bool IsKeyboardFocusableCore() => Owner.Focusable; - protected override void SetFocusCore() => Owner.Focus(); - protected override bool ShowContextMenuCore() { var c = Owner; @@ -142,6 +148,17 @@ protected internal override bool TrySetParent(AutomationPeer? parent) return true; } + protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner) ?? Owner.Name; + protected override Rect GetBoundingRectangleCore() => GetBounds(Owner.TransformedBounds); + protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Custom; + 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.IsEnabled; + protected override bool IsKeyboardFocusableCore() => Owner.Focusable; + protected override void SetFocusCore() => Owner.Focus(); + private Rect GetBounds(TransformedBounds? bounds) { return bounds?.Bounds.TransformToAABB(bounds!.Value.Transform) ?? default; diff --git a/src/Avalonia.Controls/Automation/Peers/TextAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs similarity index 67% rename from src/Avalonia.Controls/Automation/Peers/TextAutomationPeer.cs rename to src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs index c523feb0f8a..a2ddedffc63 100644 --- a/src/Avalonia.Controls/Automation/Peers/TextAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs @@ -5,19 +5,21 @@ namespace Avalonia.Automation.Peers { - public class TextAutomationPeer : ControlAutomationPeer + public class TextBlockAutomationPeer : ControlAutomationPeer { - public TextAutomationPeer(IAutomationNodeFactory factory, Control owner) + public TextBlockAutomationPeer(IAutomationNodeFactory factory, TextBlock owner) : base(factory, owner) { } + public new TextBlock Owner => (TextBlock)base.Owner; + protected override AutomationControlType GetAutomationControlTypeCore() { return AutomationControlType.Text; } - protected override string? GetNameCore() => Owner.GetValue(TextBlock.TextProperty); + protected override string? GetNameCore() => Owner.Text; protected override bool IsControlElementCore() { diff --git a/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs index 8ee1aacb524..99f50ddabbe 100644 --- a/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs @@ -6,7 +6,7 @@ namespace Avalonia.Automation.Peers { - public class TextBoxAutomationPeer : TextAutomationPeer, IValueProvider + public class TextBoxAutomationPeer : ControlAutomationPeer, IValueProvider { public TextBoxAutomationPeer(IAutomationNodeFactory factory, TextBox owner) : base(factory, owner) @@ -17,5 +17,10 @@ public TextBoxAutomationPeer(IAutomationNodeFactory factory, TextBox owner) public bool IsReadOnly => Owner.IsReadOnly; public string? Value => Owner.Text; public void SetValue(string? value) => Owner.Text = value; + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Edit; + } } } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index eb23878c3d0..a80606988b3 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -536,7 +536,7 @@ protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) { - return new TextAutomationPeer(factory, this); + return new TextBlockAutomationPeer(factory, this); } private static bool IsValidMaxLines(int maxLines) => maxLines >= 0; diff --git a/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs b/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs index 3c6d9172c43..045a4f08d1b 100644 --- a/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs +++ b/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs @@ -25,5 +25,15 @@ public void AutomationId() var byName = _session.FindElementByAccessibilityId("TextBlockWithName"); var byAutomationId = _session.FindElementByAccessibilityId("TextBlockWithNameAndAutomationId"); } + + [Fact] + public void LabeledBy() + { + var label = _session.FindElementByAccessibilityId("TextBlockAsLabel"); + var labeledTextBox = _session.FindElementByAccessibilityId("LabeledByTextBox"); + + Assert.Equal("Label for TextBox", label.Text); + Assert.Equal("Label for TextBox", labeledTextBox.GetAttribute("Name")); + } } } From 06ed8963176c0958f1612957ffcdfcc3e62dc643 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 Mar 2021 21:27:48 +0100 Subject: [PATCH 08/73] Added access/accelerator key support. And start adding integration tests for menu items. --- samples/IntegrationTestApp/MainWindow.axaml | 108 ++++++++++-------- .../Automation/Peers/AutomationPeer.cs | 16 ++- .../Automation/Peers/ButtonAutomationPeer.cs | 16 ++- .../Peers/ComboBoxAutomationPeer.cs | 2 + .../Automation/Peers/ControlAutomationPeer.cs | 2 + .../Peers/MenuItemAutomationPeer.cs | 37 ++++-- .../Primitives/AccessText.cs | 38 ++++++ .../Automation/AutomationNode.cs | 2 + .../ButtonTests.cs | 8 ++ .../MenuTests.cs | 31 +++++ 10 files changed, 201 insertions(+), 59 deletions(-) create mode 100644 tests/Avalonia.IntegrationTests.Win32/MenuTests.cs diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 0ccca5d4cc0..c72d45783c4 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -5,56 +5,64 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="IntegrationTestApp.MainWindow" Title="IntegrationTestApp"> - - - - TextBlockWithName - - TextBlockWithNameAndAutomationId - - Label for TextBox - - Foo - - - - - - - - - - + + + + + + + + + + TextBlockWithName + + TextBlockWithNameAndAutomationId + + Label for TextBox + + Foo + + + + + + + + + + + - - - Unchecked - Checked - ThreeState - - + + + Unchecked + Checked + ThreeState + + - - - - Foo - Bar - - - Foo - Bar - - - Foo - Bar - - - - + + + + Foo + Bar + + + Foo + Bar + + + Foo + Bar + + + + + diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index 8a306613591..4c517fcd719 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Globalization; using Avalonia.Automation.Platform; @@ -75,6 +76,17 @@ protected AutomationPeer(IAutomationNodeFactory factory) /// public void BringIntoView() => BringIntoViewCore(); + /// + /// Gets the accelerator key combinations for the element that is associated with the UI + /// Automation peer. + /// + public string? GetAcceleratorKey() => GetAcceleratorKeyCore(); + + /// + /// Gets the access key for the element that is associated with the automation peer. + /// + public string? GetAccessKey() => GetAccessKeyCore(); + /// /// Gets the control type for the element that is associated with the UI Automation peer. /// @@ -208,6 +220,8 @@ protected virtual string GetLocalizedControlTypeCore() } protected abstract void BringIntoViewCore(); + protected abstract string? GetAcceleratorKeyCore(); + protected abstract string? GetAccessKeyCore(); protected abstract AutomationControlType GetAutomationControlTypeCore(); protected abstract string? GetAutomationIdCore(); protected abstract Rect GetBoundingRectangleCore(); diff --git a/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs index 89c80e1144c..66e5bd7b491 100644 --- a/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs @@ -13,13 +13,27 @@ public ButtonAutomationPeer(IAutomationNodeFactory factory, Button owner) : base(factory, owner) { } - + + public new Button Owner => (Button)base.Owner; + public void Invoke() { EnsureEnabled(); (Owner as Button)?.PerformClick(); } + protected override string? GetAcceleratorKeyCore() + { + var result = base.GetAcceleratorKeyCore(); + + if (string.IsNullOrWhiteSpace(result)) + { + result = Owner.HotKey?.ToString(); + } + + return result; + } + protected override AutomationControlType GetAutomationControlTypeCore() { return AutomationControlType.Button; diff --git a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs index 721272c83ec..d11946b27e0 100644 --- a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs @@ -92,6 +92,8 @@ public object? Item } } + protected override string? GetAcceleratorKeyCore() => null; + protected override string? GetAccessKeyCore() => null; protected override string? GetAutomationIdCore() => null; protected override string GetClassNameCore() => typeof(ComboBoxItem).Name; protected override AutomationPeer? GetLabeledByCore() => null; diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index e68bf2ea471..cba5b4d79c4 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -148,6 +148,8 @@ protected internal override bool TrySetParent(AutomationPeer? parent) return true; } + protected override string? GetAcceleratorKeyCore() => AutomationProperties.GetAcceleratorKey(Owner); + protected override string? GetAccessKeyCore() => AutomationProperties.GetAccessKey(Owner); protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner) ?? Owner.Name; protected override Rect GetBoundingRectangleCore() => GetBounds(Owner.TransformedBounds); protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Custom; diff --git a/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs index b1f7774fc93..b9f032ef0dc 100644 --- a/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs @@ -1,5 +1,6 @@ using Avalonia.Automation.Platform; using Avalonia.Controls; +using Avalonia.Controls.Primitives; #nullable enable @@ -14,6 +15,33 @@ public MenuItemAutomationPeer(IAutomationNodeFactory factory, MenuItem owner) public new MenuItem Owner => (MenuItem)base.Owner; + protected override string? GetAccessKeyCore() + { + var result = base.GetAccessKeyCore(); + + if (string.IsNullOrWhiteSpace(result)) + { + if (Owner.HeaderPresenter.Child is AccessText accessText) + { + result = accessText.AccessKey.ToString(); + } + } + + return result; + } + + protected override string? GetAcceleratorKeyCore() + { + var result = base.GetAcceleratorKeyCore(); + + if (string.IsNullOrWhiteSpace(result)) + { + result = Owner.InputGesture?.ToString(); + } + + return result; + } + protected override AutomationControlType GetAutomationControlTypeCore() { return AutomationControlType.MenuItem; @@ -23,14 +51,9 @@ protected override AutomationControlType GetAutomationControlTypeCore() { var result = base.GetNameCore(); - if (result is null && Owner.HeaderPresenter.Child is TextBlock text) - { - result = text.Text; - } - - if (result is null) + if (result is null && Owner.Header is string header) { - result = Owner.Header?.ToString(); + result = AccessText.RemoveAccessKeyMarker(header); } return result; diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 7a5e6ce4266..061fd359f15 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -1,4 +1,6 @@ using System; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Input; using Avalonia.Media; using Avalonia.Media.TextFormatting; @@ -76,6 +78,37 @@ public override void Render(DrawingContext context) } } + internal static string RemoveAccessKeyMarker(string text) + { + if (!string.IsNullOrEmpty(text)) + { + var accessKeyMarker = "_"; + var doubleAccessKeyMarker = accessKeyMarker + accessKeyMarker; + int index = FindAccessKeyMarker(text); + if (index >= 0 && index < text.Length - 1) + text = text.Remove(index, 1); + text = text.Replace(doubleAccessKeyMarker, accessKeyMarker); + } + return text; + } + + private static int FindAccessKeyMarker(string text) + { + var length = text.Length; + var startIndex = 0; + while (startIndex < length) + { + int index = text.IndexOf('_', startIndex); + if (index == -1) + return -1; + if (index + 1 < length && text[index + 1] != '_') + return index; + startIndex = index + 2; + } + + return -1; + } + /// /// Get the pixel location relative to the top-left of the layout box given the text position. /// @@ -180,6 +213,11 @@ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e } } + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new NoneAutomationPeer(factory, this); + } + /// /// Returns a string with the first underscore stripped. /// diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index 3757958cca1..8a5488b1f18 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -116,6 +116,8 @@ public void PropertyChanged(AutomationProperty property, object? oldValue, objec { return (UiaPropertyId)propertyId switch { + UiaPropertyId.AcceleratorKey => InvokeSync(() => Peer.GetAcceleratorKey()), + UiaPropertyId.AccessKey => InvokeSync(() => Peer.GetAccessKey()), UiaPropertyId.AutomationId => InvokeSync(() => Peer.GetAutomationId()), UiaPropertyId.ClassName => InvokeSync(() => Peer.GetClassName()), UiaPropertyId.ClickablePoint => new[] { BoundingRectangle.Center.X, BoundingRectangle.Center.Y }, diff --git a/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs b/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs index b0d300e9fe0..21c2b1a7e3f 100644 --- a/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs +++ b/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs @@ -42,5 +42,13 @@ public void ButtonWithTextBlock() Assert.Equal("Button with TextBlock", button.Text); } + + [Fact] + public void ButtonWithAcceleratorKey() + { + var button = _session.FindElementByAccessibilityId("ButtonWithAcceleratorKey"); + + Assert.Equal("Ctrl+B", button.GetAttribute("AcceleratorKey")); + } } } diff --git a/tests/Avalonia.IntegrationTests.Win32/MenuTests.cs b/tests/Avalonia.IntegrationTests.Win32/MenuTests.cs new file mode 100644 index 00000000000..3d93afec128 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Win32/MenuTests.cs @@ -0,0 +1,31 @@ +using OpenQA.Selenium.Appium.Windows; +using Xunit; + +namespace Avalonia.IntegrationTests.Win32 +{ + [Collection("Default")] + public class MenuTests + { + private WindowsDriver _session; + + public MenuTests(TestAppFixture fixture) => _session = fixture.Session; + + [Fact] + public void File() + { + var fileMenu = _session.FindElementByAccessibilityId("FileMenu"); + + Assert.Equal("File", fileMenu.Text); + } + + [Fact] + public void Open() + { + var fileMenu = _session.FindElementByAccessibilityId("FileMenu"); + fileMenu.Click(); + + var openMenu = fileMenu.FindElementByAccessibilityId("OpenMenu"); + Assert.Equal("Ctrl+O", openMenu.GetAttribute("AcceleratorKey")); + } + } +} From 3fcfe4d9de1e42e66eec1eccafe0f0279d875ae6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 Mar 2021 22:55:33 +0100 Subject: [PATCH 09/73] Notify UIA that a window has been destroyed. --- src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index b0d41b72eb6..8d3a17bcc5c 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -76,6 +76,9 @@ protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam, case WindowsMessage.WM_DESTROY: { + if (_automationProvider is object) + UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, IntPtr.Zero, IntPtr.Zero, null); + //Window doesn't exist anymore _hwnd = IntPtr.Zero; //Remove root reference to this class, so unmanaged delegate can be collected From bd0d97309294b275648d50130eddf7c74e8cae27 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 Mar 2021 23:19:09 +0100 Subject: [PATCH 10/73] Use Group as generic control type. Seems to be what best fits, and what e.g. chrome uses. --- src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs index 894234e5023..fa5e6c175e0 100644 --- a/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs @@ -18,7 +18,7 @@ public NoneAutomationPeer(IAutomationNodeFactory factory, Control owner) protected override AutomationControlType GetAutomationControlTypeCore() { - return AutomationControlType.Pane; + return AutomationControlType.Group; } protected override bool IsContentElementCore() => false; From acc40a7e21641ec5eb42ba6fa1c562a178813e92 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 Mar 2021 23:29:02 +0100 Subject: [PATCH 11/73] Add context menu automation peer. And fixed NRE in MenuItemAutomationPeer. --- .../Peers/ContextMenuAutomationPeer.cs | 22 +++++++++++++++++++ .../Peers/MenuItemAutomationPeer.cs | 2 +- src/Avalonia.Controls/ContextMenu.cs | 7 ++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Controls/Automation/Peers/ContextMenuAutomationPeer.cs diff --git a/src/Avalonia.Controls/Automation/Peers/ContextMenuAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ContextMenuAutomationPeer.cs new file mode 100644 index 00000000000..5631fcf5811 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ContextMenuAutomationPeer.cs @@ -0,0 +1,22 @@ +using Avalonia.Automation.Platform; +using Avalonia.Controls; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class ContextMenuAutomationPeer : ControlAutomationPeer + { + public ContextMenuAutomationPeer(IAutomationNodeFactory factory, ContextMenu owner) + : base(factory, owner) + { + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Menu; + } + + protected override bool IsContentElementCore() => false; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs index b9f032ef0dc..5ed1c5e5790 100644 --- a/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs @@ -21,7 +21,7 @@ public MenuItemAutomationPeer(IAutomationNodeFactory factory, MenuItem owner) if (string.IsNullOrWhiteSpace(result)) { - if (Owner.HeaderPresenter.Child is AccessText accessText) + if (Owner.HeaderPresenter?.Child is AccessText accessText) { result = accessText.AccessKey.ToString(); } diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 57e4909e39a..c190e98f542 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; @@ -304,6 +306,11 @@ protected override IItemContainerGenerator CreateItemContainerGenerator() return new MenuItemContainerGenerator(this); } + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new ContextMenuAutomationPeer(factory, this); + } + private void Open(Control control, Control placementTarget) { if (IsOpen) From 7f4222997b30c00fb5da6720a13bd90c8bdb5744 Mon Sep 17 00:00:00 2001 From: grokys Date: Fri, 12 Mar 2021 15:24:56 +0100 Subject: [PATCH 12/73] Don't use C#9 features. Causing the build to fail on OSX/Linux --- .../Automation/AutomationElementIdentifiers.cs | 6 +++--- .../Automation/ExpandCollapsePatternIdentifiers.cs | 2 +- .../Automation/RangeValuePatternIdentifiers.cs | 8 ++++---- .../Automation/ScrollPatternIdentifiers.cs | 12 ++++++------ .../Automation/SelectionPatternIdentifiers.cs | 6 +++--- .../Avalonia.Win32/Automation/AutomationNode.cs | 4 ++-- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs b/src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs index 7f6bff11ad3..4566cd9db5a 100644 --- a/src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs +++ b/src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs @@ -11,18 +11,18 @@ public static class AutomationElementIdentifiers /// Identifies the bounding rectangle automation property. The bounding rectangle property /// value is returned by the method. /// - public static AutomationProperty BoundingRectangleProperty { get; } = new(); + public static AutomationProperty BoundingRectangleProperty { get; } = new AutomationProperty(); /// /// Identifies the class name automation property. The class name property value is returned /// by the method. /// - public static AutomationProperty ClassNameProperty { get; } = new(); + public static AutomationProperty ClassNameProperty { get; } = new AutomationProperty(); /// /// Identifies the name automation property. The class name property value is returned /// by the method. /// - public static AutomationProperty NameProperty { get; } = new(); + public static AutomationProperty NameProperty { get; } = new AutomationProperty(); } } diff --git a/src/Avalonia.Controls/Automation/ExpandCollapsePatternIdentifiers.cs b/src/Avalonia.Controls/Automation/ExpandCollapsePatternIdentifiers.cs index 342bdaceb12..e2b67821620 100644 --- a/src/Avalonia.Controls/Automation/ExpandCollapsePatternIdentifiers.cs +++ b/src/Avalonia.Controls/Automation/ExpandCollapsePatternIdentifiers.cs @@ -10,6 +10,6 @@ public static class ExpandCollapsePatternIdentifiers /// /// Identifies automation property. /// - public static AutomationProperty ExpandCollapseStateProperty { get; } = new(); + public static AutomationProperty ExpandCollapseStateProperty { get; } = new AutomationProperty(); } } diff --git a/src/Avalonia.Controls/Automation/RangeValuePatternIdentifiers.cs b/src/Avalonia.Controls/Automation/RangeValuePatternIdentifiers.cs index e3fa744cc02..625b37d0017 100644 --- a/src/Avalonia.Controls/Automation/RangeValuePatternIdentifiers.cs +++ b/src/Avalonia.Controls/Automation/RangeValuePatternIdentifiers.cs @@ -10,21 +10,21 @@ public static class RangeValuePatternIdentifiers /// /// Identifies automation property. /// - public static AutomationProperty IsReadOnlyProperty { get; } = new(); + public static AutomationProperty IsReadOnlyProperty { get; } = new AutomationProperty(); /// /// Identifies automation property. /// - public static AutomationProperty MinimumProperty { get; } = new(); + public static AutomationProperty MinimumProperty { get; } = new AutomationProperty(); /// /// Identifies automation property. /// - public static AutomationProperty MaximumProperty { get; } = new(); + public static AutomationProperty MaximumProperty { get; } = new AutomationProperty(); /// /// Identifies automation property. /// - public static AutomationProperty ValueProperty { get; } = new(); + public static AutomationProperty ValueProperty { get; } = new AutomationProperty(); } } diff --git a/src/Avalonia.Controls/Automation/ScrollPatternIdentifiers.cs b/src/Avalonia.Controls/Automation/ScrollPatternIdentifiers.cs index ad10b96e17a..d9e843e75aa 100644 --- a/src/Avalonia.Controls/Automation/ScrollPatternIdentifiers.cs +++ b/src/Avalonia.Controls/Automation/ScrollPatternIdentifiers.cs @@ -15,31 +15,31 @@ public static class ScrollPatternIdentifiers /// /// Identifies automation property. /// - public static AutomationProperty HorizontallyScrollableProperty { get; } = new(); + public static AutomationProperty HorizontallyScrollableProperty { get; } = new AutomationProperty(); /// /// Identifies automation property. /// - public static AutomationProperty HorizontalScrollPercentProperty { get; } = new(); + public static AutomationProperty HorizontalScrollPercentProperty { get; } = new AutomationProperty(); /// /// Identifies automation property. /// - public static AutomationProperty HorizontalViewSizeProperty { get; } = new(); + public static AutomationProperty HorizontalViewSizeProperty { get; } = new AutomationProperty(); /// /// Identifies automation property. /// - public static AutomationProperty VerticallyScrollableProperty { get; } = new(); + public static AutomationProperty VerticallyScrollableProperty { get; } = new AutomationProperty(); /// /// Identifies automation property. /// - public static AutomationProperty VerticalScrollPercentProperty { get; } = new(); + public static AutomationProperty VerticalScrollPercentProperty { get; } = new AutomationProperty(); /// /// Identifies automation property. /// - public static AutomationProperty VerticalViewSizeProperty { get; } = new(); + public static AutomationProperty VerticalViewSizeProperty { get; } = new AutomationProperty(); } } diff --git a/src/Avalonia.Controls/Automation/SelectionPatternIdentifiers.cs b/src/Avalonia.Controls/Automation/SelectionPatternIdentifiers.cs index ce4fdda7398..c3669528cd7 100644 --- a/src/Avalonia.Controls/Automation/SelectionPatternIdentifiers.cs +++ b/src/Avalonia.Controls/Automation/SelectionPatternIdentifiers.cs @@ -10,16 +10,16 @@ public static class SelectionPatternIdentifiers /// /// Identifies automation property. /// - public static AutomationProperty CanSelectMultipleProperty { get; } = new(); + public static AutomationProperty CanSelectMultipleProperty { get; } = new AutomationProperty(); /// /// Identifies automation property. /// - public static AutomationProperty IsSelectionRequiredProperty { get; } = new(); + public static AutomationProperty IsSelectionRequiredProperty { get; } = new AutomationProperty(); /// /// Identifies the property that gets the selected items in a container. /// - public static AutomationProperty SelectionProperty { get; } = new(); + public static AutomationProperty SelectionProperty { get; } = new AutomationProperty(); } } diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index 8a5488b1f18..c7021e6d533 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -25,7 +25,7 @@ internal partial class AutomationNode : MarshalByRefObject, IRawElementProviderAdviseEvents, IInvokeProvider { - private static Dictionary s_propertyMap = new() + private static Dictionary s_propertyMap = new Dictionary() { { AutomationElementIdentifiers.BoundingRectangleProperty, UiaPropertyId.BoundingRectangle }, { AutomationElementIdentifiers.ClassNameProperty, UiaPropertyId.ClassName }, @@ -276,7 +276,7 @@ private AutomationPeer GetRoot() var peer = Peer; var parent = peer.GetParent(); - while (peer is not AAP.IRootProvider && parent is object) + while (!(peer is AAP.IRootProvider) && parent is object) { peer = parent; parent = peer.GetParent(); From 8add2371c6da37a69d3fe4d3ca56785b12b5e630 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 23 Mar 2021 22:55:39 +0100 Subject: [PATCH 13/73] Make root automation node come from OS. This will be needed for OSX. --- .../Automation/Peers/AutomationPeer.cs | 11 ++++++++++ .../Automation/Peers/ControlAutomationPeer.cs | 19 +++++++++++++---- .../Peers/PopupRootAutomationPeer.cs | 4 ++-- .../Automation/Peers/WindowAutomationPeer.cs | 4 ++-- .../Peers/WindowBaseAutomationPeer.cs | 4 ++-- src/Avalonia.Controls/Control.cs | 7 +++++++ .../Platform/IWindowBaseImpl.cs | 7 +++++++ src/Avalonia.Controls/Primitives/PopupRoot.cs | 4 ++-- src/Avalonia.Controls/TopLevel.cs | 2 ++ src/Avalonia.Controls/Window.cs | 4 ++-- src/Avalonia.Controls/WindowBase.cs | 21 +++++++++++++++++++ .../Remote/PreviewerWindowImpl.cs | 3 +++ src/Avalonia.DesignerSupport/Remote/Stubs.cs | 3 +++ src/Avalonia.Headless/HeadlessWindowImpl.cs | 3 +++ src/Avalonia.Native/WindowImplBase.cs | 3 +++ src/Avalonia.X11/X11Window.cs | 3 +++ .../Automation/AutomationNode.cs | 8 ++++++- .../Automation/AutomationNodeFactory.cs | 3 +-- .../Automation/RootAutomationNode.cs | 4 ++-- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 12 +++++++---- src/Windows/Avalonia.Win32/WindowImpl.cs | 14 ++----------- 21 files changed, 108 insertions(+), 35 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index 4c517fcd719..25af3400147 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -66,6 +66,17 @@ protected AutomationPeer(IAutomationNodeFactory factory) Node = factory.CreateNode(this); } + /// + /// Initializes a new instance of the class. + /// + /// + /// The platform automation node. + /// + protected AutomationPeer(IAutomationNode node) + { + Node = node; + } + /// /// Gets the related node in the platform UI Automation tree. /// diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index cba5b4d79c4..bd4e9dfcf41 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -23,10 +23,14 @@ public ControlAutomationPeer(IAutomationNodeFactory factory, Control owner) : base(factory) { Owner = owner ?? throw new ArgumentNullException("owner"); + Initialize(); + } - owner.PropertyChanged += OwnerPropertyChanged; - var visualChildren = ((IVisual)owner).VisualChildren; - visualChildren.CollectionChanged += VisualChildrenChanged; + protected ControlAutomationPeer(IAutomationNode node, Control owner) + : base(node) + { + Owner = owner ?? throw new ArgumentNullException("owner"); + Initialize(); } public Control Owner { get; } @@ -161,11 +165,18 @@ protected internal override bool TrySetParent(AutomationPeer? parent) protected override bool IsKeyboardFocusableCore() => Owner.Focusable; protected override void SetFocusCore() => Owner.Focus(); - private Rect GetBounds(TransformedBounds? bounds) + private static Rect GetBounds(TransformedBounds? bounds) { return bounds?.Bounds.TransformToAABB(bounds!.Value.Transform) ?? default; } + private void Initialize() + { + Owner.PropertyChanged += OwnerPropertyChanged; + var visualChildren = ((IVisual)Owner).VisualChildren; + visualChildren.CollectionChanged += VisualChildrenChanged; + } + private void VisualChildrenChanged(object sender, EventArgs e) => InvalidateChildren(); private void OwnerPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) diff --git a/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs index b54a938a7f0..f7e06e83eb5 100644 --- a/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs @@ -8,8 +8,8 @@ namespace Avalonia.Automation.Peers { public class PopupRootAutomationPeer : WindowBaseAutomationPeer { - public PopupRootAutomationPeer(IAutomationNodeFactory factory, PopupRoot owner) - : base(factory, owner) + public PopupRootAutomationPeer(IAutomationNode node, PopupRoot owner) + : base(node, owner) { if (owner.IsVisible) StartTrackingFocus(); diff --git a/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs index 41778457e80..9629ae0294b 100644 --- a/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs @@ -8,8 +8,8 @@ namespace Avalonia.Automation.Peers { public class WindowAutomationPeer : WindowBaseAutomationPeer { - public WindowAutomationPeer(IAutomationNodeFactory factory, Window owner) - : base(factory, owner) + public WindowAutomationPeer(IAutomationNode node, Window owner) + : base(node, owner) { if (owner.IsVisible) StartTrackingFocus(); diff --git a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs index ac235b38988..f97d13c766b 100644 --- a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs @@ -14,8 +14,8 @@ public class WindowBaseAutomationPeer : ControlAutomationPeer, IRootProvider { private Control? _focus; - public WindowBaseAutomationPeer(IAutomationNodeFactory factory, WindowBase owner) - : base(factory, owner) + public WindowBaseAutomationPeer(IAutomationNode node, WindowBase owner) + : base(node, owner) { } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 6b63ac14ccf..ac95743e30c 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -209,5 +209,12 @@ internal AutomationPeer GetOrCreateAutomationPeer(IAutomationNodeFactory factory _automationPeer = OnCreateAutomationPeer(factory); return _automationPeer; } + + internal void SetAutomationPeer(AutomationPeer peer) + { + if (_automationPeer is object) + throw new InvalidOperationException("Automation peer is already set."); + _automationPeer = peer ?? throw new ArgumentNullException(nameof(peer)); + } } } diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs index 0d303a6666b..8d18b925f8c 100644 --- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs @@ -1,4 +1,6 @@ using System; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; namespace Avalonia.Platform { @@ -44,6 +46,11 @@ public interface IWindowBaseImpl : ITopLevelImpl /// Action Activated { get; set; } + /// + /// Gets or sets a method called when automation is started on the window. + /// + Func AutomationStarted { get; set; } + /// /// Gets the platform window handle. /// diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 73dd68e7cbc..96ceb2afcfc 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -171,9 +171,9 @@ protected override sealed Size ArrangeSetBounds(Size size) } } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNode node) { - return new PopupRootAutomationPeer(factory, this); + return new PopupRootAutomationPeer(node, this); } } } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 7a92836ddfc..cbb9506627c 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -1,5 +1,7 @@ using System; using System.Reactive.Linq; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Input; diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 76481624933..9d54f3beee4 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -968,9 +968,9 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs } } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNode node) { - return new WindowAutomationPeer(factory, this); + return new WindowAutomationPeer(node, this); } } } diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index cdcb499e983..4f31dfeada0 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -3,6 +3,8 @@ using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Layout; @@ -64,6 +66,7 @@ public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver dependencyRe impl.Activated = HandleActivated; impl.Deactivated = HandleDeactivated; impl.PositionChanged = HandlePositionChanged; + impl.AutomationStarted = HandleAutomationStarted; } /// @@ -269,6 +272,17 @@ protected override void ArrangeCore(Rect finalRect) /// The actual size of the window. protected virtual Size ArrangeSetBounds(Size size) => size; + protected sealed override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + throw new NotSupportedException( + "Automation peer for window controls must be created by the operating system."); + } + + protected virtual AutomationPeer OnCreateAutomationPeer(IAutomationNode node) + { + throw new NotImplementedException("OnCreateAutomationPeer must be implemented in a derived class."); + } + /// /// Handles a window position change notification from /// . @@ -306,6 +320,13 @@ private void HandleDeactivated() Deactivated?.Invoke(this, EventArgs.Empty); } + private AutomationPeer HandleAutomationStarted(IAutomationNode node) + { + var peer = OnCreateAutomationPeer(node); + SetAutomationPeer(peer); + return peer; + } + private void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) { if (!_ignoreVisibilityChange) diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 787f44887fa..19195031519 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -1,5 +1,7 @@ using System; using System.Reactive.Disposables; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls; using Avalonia.Controls.Remote.Server; using Avalonia.Input; @@ -45,6 +47,7 @@ public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) public IPlatformHandle Handle { get; } public WindowState WindowState { get; set; } public Action WindowStateChanged { get; set; } + public Func AutomationStarted { get; set; } public Size MaxAutoSizeHint { get; } = new Size(4096, 4096); protected override void OnMessage(IAvaloniaRemoteTransportConnection transport, object obj) diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index eedfc52d9da..090d93362b3 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -3,6 +3,8 @@ using System.IO; using System.Reactive.Disposables; using System.Threading.Tasks; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives.PopupPositioning; @@ -38,6 +40,7 @@ class WindowStub : IWindowImpl, IPopupImpl public Action PositionChanged { get; set; } public WindowState WindowState { get; set; } public Action WindowStateChanged { get; set; } + public Func AutomationStarted { get; set; } public Action TransparencyLevelChanged { get; set; } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index af522f3e36f..fe3064b4152 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Controls.Primitives.PopupPositioning; @@ -142,6 +144,7 @@ public void SetTopmost(bool value) public IScreenImpl Screen { get; } = new HeadlessScreensStub(); public WindowState WindowState { get; set; } public Action WindowStateChanged { get; set; } + public Func AutomationStarted { get; set; } public void SetTitle(string title) { diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 71359f733d2..65b1ae21204 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Controls.Platform.Surfaces; @@ -466,5 +468,6 @@ public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0, 0); public IPlatformHandle Handle { get; private set; } + public Func AutomationStarted { get; set; } } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 8f3f412578e..3650bf1c79a 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -6,6 +6,8 @@ using System.Reactive.Disposables; using System.Text; using System.Threading.Tasks; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives.PopupPositioning; @@ -1128,5 +1130,6 @@ public void SetWindowManagerAddShadowHint(bool enabled) public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0.8); public bool NeedsManagedDecorations => false; + public Func AutomationStarted { get; set; } } } diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index c7021e6d533..3963d0cb6cc 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -56,7 +56,13 @@ public AutomationNode(AutomationPeer peer) Peer = peer; } - public AutomationPeer Peer { get; } + protected AutomationNode(Func peerGetter) + { + _runtimeId = new int[] { 3, GetHashCode() }; + Peer = peerGetter(this); + } + + public AutomationPeer Peer { get; protected set; } public IAutomationNodeFactory Factory => AutomationNodeFactory.Instance; public Rect BoundingRectangle diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNodeFactory.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNodeFactory.cs index 776b65adc61..a7ee0e192f8 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNodeFactory.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNodeFactory.cs @@ -1,6 +1,5 @@ using Avalonia.Automation.Peers; using Avalonia.Automation.Platform; -using Avalonia.Automation.Provider; using Avalonia.Threading; #nullable enable @@ -14,7 +13,7 @@ internal class AutomationNodeFactory : IAutomationNodeFactory public IAutomationNode CreateNode(AutomationPeer peer) { Dispatcher.UIThread.VerifyAccess(); - return peer is IRootProvider ? new RootAutomationNode(peer) : new AutomationNode(peer); + return new AutomationNode(peer); } } } diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs index cb8cfae90ed..dd2665a624c 100644 --- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -13,8 +13,8 @@ internal class RootAutomationNode : AutomationNode, IRawElementProviderFragmentRoot, IRootAutomationNode { - public RootAutomationNode(AutomationPeer peer) - : base(peer) + public RootAutomationNode(Func peerGetter) + : base(peerGetter) { } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 8d3a17bcc5c..31afcc368c3 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -4,6 +4,7 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Win32.Automation; using Avalonia.Win32.Input; using Avalonia.Win32.Interop.Automation; using static Avalonia.Win32.Interop.UnmanagedMethods; @@ -76,7 +77,7 @@ protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam, case WindowsMessage.WM_DESTROY: { - if (_automationProvider is object) + if (_automationNode is object) UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, IntPtr.Zero, IntPtr.Zero, null); //Window doesn't exist anymore @@ -461,11 +462,14 @@ protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam, case WindowsMessage.WM_GETOBJECT: if ((long)lParam == UiaRootObjectId) { - var provider = GetOrCreateAutomationProvider(); + if (_automationNode is null && AutomationStarted is object) + { + _automationNode = new RootAutomationNode(AutomationStarted); + } - if (provider is object) + if (_automationNode is object) { - var r = UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, wParam, lParam, provider); + var r = UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, wParam, lParam, _automationNode); return r; } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 02fbb7a1431..fa3d2665ed7 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -87,7 +87,7 @@ public partial class WindowImpl : IWindowImpl, private POINT _maxTrackSize; private WindowImpl _parent; private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default; - private AutomationNode _automationProvider; + private AutomationNode _automationNode; private bool _isCloseRequested; private bool _shown; @@ -175,6 +175,7 @@ egl.Display is AngleWin32EglDisplay angleDisplay && public Action LostFocus { get; set; } public Action TransparencyLevelChanged { get; set; } + public Func AutomationStarted { get; set; } public Thickness BorderThickness { @@ -1269,17 +1270,6 @@ public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) /// public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0); - internal AutomationNode GetOrCreateAutomationProvider() - { - if (_automationProvider is null) - { - var peer = ControlAutomationPeer.GetOrCreatePeer(AutomationNodeFactory.Instance, (Control)_owner); - _automationProvider = peer.Node as AutomationNode; - } - - return _automationProvider; - } - private struct SavedWindowInfo { public WindowStyles Style { get; set; } From 1c603747b56d92c674583c0ecb6d7c5b12c5e3dd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 26 Mar 2021 20:52:20 +0100 Subject: [PATCH 14/73] Use correct peer for TabItems. --- src/Avalonia.Controls/TabItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 4d50ef961d6..90817c655de 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -85,7 +85,7 @@ private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj) protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) { - return new ListItemAutomationPeer(factory, this); + return new TabItemAutomationPeer(factory, this); } } } From 7bb26473d13b5352ee6b8f30afa5d2c7bbd1a867 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 31 May 2021 13:47:20 +0200 Subject: [PATCH 15/73] Fix AccessText after merge. --- .../Primitives/AccessText.cs | 52 +++++++++---------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 8a269287b20..15c44e8c125 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -76,26 +76,12 @@ public override void Render(DrawingContext context) rect.BottomLeft + offset, rect.BottomRight + offset); } - - internal static string RemoveAccessKeyMarker(string text) - { - if (!string.IsNullOrEmpty(text)) - { - var accessKeyMarker = "_"; - var doubleAccessKeyMarker = accessKeyMarker + accessKeyMarker; - int index = FindAccessKeyMarker(text); - if (index >= 0 && index < text.Length - 1) - text = text.Remove(index, 1); - text = text.Replace(doubleAccessKeyMarker, accessKeyMarker); - } - return text; - } } /// protected override TextLayout CreateTextLayout(Size constraint, string text) { - return base.CreateTextLayout(constraint, StripAccessKey(text)); + return base.CreateTextLayout(constraint, RemoveAccessKeyMarker(text)); } /// @@ -127,23 +113,35 @@ protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory return new NoneAutomationPeer(factory, this); } - /// - /// Returns a string with the first underscore stripped. - /// - /// The text. - /// The text with the first underscore stripped. - private string StripAccessKey(string text) + internal static string RemoveAccessKeyMarker(string text) { - var position = text.IndexOf('_'); - - if (position == -1) + if (!string.IsNullOrEmpty(text)) { - return text; + var accessKeyMarker = "_"; + var doubleAccessKeyMarker = accessKeyMarker + accessKeyMarker; + int index = FindAccessKeyMarker(text); + if (index >= 0 && index < text.Length - 1) + text = text.Remove(index, 1); + text = text.Replace(doubleAccessKeyMarker, accessKeyMarker); } - else + return text; + } + + private static int FindAccessKeyMarker(string text) + { + var length = text.Length; + var startIndex = 0; + while (startIndex < length) { - return text.Substring(0, position) + text.Substring(position + 1); + int index = text.IndexOf('_', startIndex); + if (index == -1) + return -1; + if (index + 1 < length && text[index + 1] != '_') + return index; + startIndex = index + 2; } + + return -1; } /// From d6d583a16ea3ae68cebd02a12160dfdc48189cc2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 26 Mar 2021 18:31:51 +0100 Subject: [PATCH 16/73] Initial implementation of OSX automation. --- .../project.pbxproj | 8 + native/Avalonia.Native/src/OSX/AvnString.h | 1 + native/Avalonia.Native/src/OSX/AvnString.mm | 17 ++ native/Avalonia.Native/src/OSX/automation.h | 16 ++ native/Avalonia.Native/src/OSX/automation.mm | 220 ++++++++++++++++++ native/Avalonia.Native/src/OSX/common.h | 4 + native/Avalonia.Native/src/OSX/main.mm | 16 ++ native/Avalonia.Native/src/OSX/window.h | 1 + native/Avalonia.Native/src/OSX/window.mm | 83 ++++++- src/Avalonia.Native/AutomationNode.cs | 36 +++ src/Avalonia.Native/AutomationNodeFactory.cs | 24 ++ src/Avalonia.Native/AvnAutomationPeer.cs | 97 ++++++++ src/Avalonia.Native/AvnString.cs | 48 ++++ src/Avalonia.Native/Helpers.cs | 11 + src/Avalonia.Native/PopupImpl.cs | 4 +- src/Avalonia.Native/WindowImpl.cs | 4 +- src/Avalonia.Native/WindowImplBase.cs | 15 +- src/Avalonia.Native/avn.idl | 88 +++++++ 18 files changed, 684 insertions(+), 9 deletions(-) create mode 100644 native/Avalonia.Native/src/OSX/automation.h create mode 100644 native/Avalonia.Native/src/OSX/automation.mm create mode 100644 src/Avalonia.Native/AutomationNode.cs create mode 100644 src/Avalonia.Native/AutomationNodeFactory.cs create mode 100644 src/Avalonia.Native/AvnAutomationPeer.cs diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index dba3ee6d31d..2a3f7d7e7dc 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -29,6 +29,8 @@ AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB661C1D2148230F00291242 /* AppKit.framework */; }; AB661C202148286E00291242 /* window.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB661C1F2148286E00291242 /* window.mm */; }; AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */; }; + BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; }; + BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -61,6 +63,8 @@ AB661C212148288600291242 /* common.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = common.h; sourceTree = ""; }; AB7A61EF2147C815003C5833 /* libAvalonia.Native.OSX.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libAvalonia.Native.OSX.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = platformthreading.mm; sourceTree = ""; }; + BC11A5BC2608D58F0017BAD0 /* automation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = automation.h; sourceTree = ""; }; + BC11A5BD2608D58F0017BAD0 /* automation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = automation.mm; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -94,6 +98,8 @@ AB7A61E62147C814003C5833 = { isa = PBXGroup; children = ( + BC11A5BC2608D58F0017BAD0 /* automation.h */, + BC11A5BD2608D58F0017BAD0 /* automation.mm */, 1A1852DB23E05814008F0DED /* deadlock.mm */, 1A002B9D232135EE00021753 /* app.mm */, 37DDA9B121933371002E132B /* AvnString.h */, @@ -138,6 +144,7 @@ buildActionMask = 2147483647; files = ( 37155CE4233C00EB0034DCE9 /* menu.h in Headers */, + BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -207,6 +214,7 @@ AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */, 1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */, 1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */, + BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */, 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */, 520624B322973F4100C4DCEF /* menu.mm in Sources */, 37A517B32159597E00FBA241 /* Screens.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/AvnString.h b/native/Avalonia.Native/src/OSX/AvnString.h index 3ce83d370a7..3b750b11dba 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.h +++ b/native/Avalonia.Native/src/OSX/AvnString.h @@ -14,4 +14,5 @@ extern IAvnStringArray* CreateAvnStringArray(NSArray* array); extern IAvnStringArray* CreateAvnStringArray(NSArray* array); extern IAvnStringArray* CreateAvnStringArray(NSString* string); extern IAvnString* CreateByteArray(void* data, int len); +extern NSString* GetNSStringAndRelease(IAvnString* s); #endif /* AvnString_h */ diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index 001cf151d87..df11ad87150 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -141,3 +141,20 @@ virtual HRESULT Get(unsigned int index, IAvnString**ppv) override { return new AvnStringImpl(data, len); } + +NSString* GetNSStringAndRelease(IAvnString* s) +{ + if (s != nullptr) + { + char* p; + + if (s->Pointer((void**)&p) == S_OK) + { + return [NSString stringWithUTF8String:p]; + } + + s->Release(); + } + + return nullptr; +} diff --git a/native/Avalonia.Native/src/OSX/automation.h b/native/Avalonia.Native/src/OSX/automation.h new file mode 100644 index 00000000000..65e1153248d --- /dev/null +++ b/native/Avalonia.Native/src/OSX/automation.h @@ -0,0 +1,16 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +class IAvnAutomationPeer; + +@interface AvnAutomationNode : NSAccessibilityElement +- (AvnAutomationNode *)initWithPeer:(IAvnAutomationPeer *)peer; +@end + +struct INSAccessibilityHolder +{ + virtual NSObject* _Nonnull GetNSAccessibility () = 0; +}; + +NS_ASSUME_NONNULL_END diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm new file mode 100644 index 00000000000..064b4140941 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -0,0 +1,220 @@ +#import "automation.h" +#include "common.h" +#include "AvnString.h" +#include "window.h" + +class AutomationNode : public ComSingleObject, + public INSAccessibilityHolder +{ +private: + NSAccessibilityElement* _node; +public: + FORWARD_IUNKNOWN() + + AutomationNode(NSAccessibilityElement* node) + { + _node = node; + } + + AutomationNode(IAvnAutomationPeer* peer) + { + _node = [[AvnAutomationNode alloc] initWithPeer: peer]; + } + + virtual NSObject* GetNSAccessibility() override + { + return _node; + } +}; + +@implementation AvnAutomationNode +{ + IAvnAutomationPeer* _peer; + NSMutableArray* _children; +} + +- (AvnAutomationNode *)initWithPeer:(IAvnAutomationPeer *)peer +{ + self = [super init]; + _peer = peer; + return self; +} + +- (BOOL)isAccessibilityElement +{ + return _peer->IsControlElement(); +} + +- (NSAccessibilityRole)accessibilityRole +{ + auto controlType = _peer->GetAutomationControlType(); + + switch (controlType) { + case AutomationButton: + return NSAccessibilityButtonRole; + case AutomationCheckBox: + return NSAccessibilityCheckBoxRole; + case AutomationComboBox: + return NSAccessibilityPopUpButtonRole; + case AutomationGroup: + case AutomationPane: + return NSAccessibilityGroupRole; + case AutomationSlider: + return NSAccessibilitySliderRole; + case AutomationTab: + return NSAccessibilityTabGroupRole; + case AutomationTabItem: + return NSAccessibilityRadioButtonRole; + case AutomationWindow: + return NSAccessibilityWindowRole; + default: + return NSAccessibilityUnknownRole; + } +} + +- (NSString *)accessibilityIdentifier +{ + return GetNSStringAndRelease(_peer->GetAutomationId()); +} + +- (NSString *)accessibilityTitle +{ + return GetNSStringAndRelease(_peer->GetName()); +} + +- (NSArray *)accessibilityChildren +{ + if (_children == nullptr && _peer != nullptr) + { + auto childPeers = _peer->GetChildren(); + auto childCount = childPeers != nullptr ? childPeers->GetCount() : 0; + + if (childCount > 0) + { + _children = [[NSMutableArray alloc] initWithCapacity:childCount]; + + for (int i = 0; i < childCount; ++i) + { + IAvnAutomationPeer* child; + + if (childPeers->Get(i, &child) == S_OK) + { + NSObject* element = ::GetAccessibilityElement(child->GetNode()); + [_children addObject:element]; + } + } + } + } + + return _children; +} + +- (NSRect)accessibilityFrame +{ + auto view = [self getAvnView]; + auto window = [self getAvnWindow]; + + if (view != nullptr) + { + auto bounds = ToNSRect(_peer->GetBoundingRectangle()); + auto windowBounds = [view convertRect:bounds toView:nil]; + auto screenBounds = [window convertRectToScreen:windowBounds]; + return screenBounds; + } + + return NSRect(); +} + +- (id)accessibilityParent +{ + auto parentPeer = _peer->GetParent(); + + if (parentPeer != nullptr) + { + return GetAccessibilityElement(parentPeer); + } + + return [NSApplication sharedApplication]; +} + +- (id)accessibilityTopLevelUIElement +{ + return GetAccessibilityElement([self getRootNode]); +} + +- (id)accessibilityWindow +{ + return [self accessibilityTopLevelUIElement]; +} + +- (BOOL)accessibilityPerformPress +{ + _peer->InvokeProvider_Invoke(); + return YES; +} + +- (BOOL)isAccessibilitySelectorAllowed:(SEL)selector +{ + if (selector == @selector(accessibilityPerformPress)) + { + return _peer->IsInvokeProvider(); + } + + return [super isAccessibilitySelectorAllowed:selector]; +} + +- (IAvnAutomationNode*)getRootNode +{ + auto rootPeer = _peer->GetRootPeer(); + return rootPeer != nullptr ? rootPeer->GetNode() : nullptr; +} + +- (IAvnWindowBase*)getWindow +{ + auto rootNode = [self getRootNode]; + + if (rootNode != nullptr) + { + IAvnWindowBase* window; + if (rootNode->QueryInterface(&IID_IAvnWindow, (void**)&window) == S_OK) + { + return window; + } + } + + return nullptr; +} + +- (AvnWindow*) getAvnWindow +{ + auto window = [self getWindow]; + return dynamic_cast(window)->GetNSWindow(); +} + +- (AvnView*) getAvnView +{ + auto window = [self getWindow]; + return dynamic_cast(window)->GetNSView(); +} + +@end + +extern IAvnAutomationNode* CreateAutomationNode(IAvnAutomationPeer* peer) +{ + @autoreleasepool + { + return new AutomationNode(peer); + } +} + +extern NSObject* GetAccessibilityElement(IAvnAutomationPeer* peer) +{ + auto node = peer != nullptr ? peer->GetNode() : nullptr; + return GetAccessibilityElement(node); +} + +extern NSObject* GetAccessibilityElement(IAvnAutomationNode* node) +{ + auto holder = dynamic_cast(node); + return holder != nullptr ? holder->GetNSAccessibility() : nil; +} diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index c082003ccfd..7c00ebc4691 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -30,10 +30,14 @@ extern IAvnMenu* GetAppMenu (); extern NSMenuItem* GetAppMenuItem (); extern void SetAutoGenerateDefaultAppMenuItems (bool enabled); extern bool GetAutoGenerateDefaultAppMenuItems (); +extern IAvnAutomationNode* CreateAutomationNode(IAvnAutomationPeer* peer); +extern NSObject* GetAccessibilityElement(IAvnAutomationPeer* peer); +extern NSObject* GetAccessibilityElement(IAvnAutomationNode* node); extern void InitializeAvnApp(IAvnApplicationEvents* events); extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; extern NSPoint ToNSPoint (AvnPoint p); +extern NSRect ToNSRect (AvnRect r); extern AvnPoint ToAvnPoint (NSPoint p); extern AvnPoint ConvertPointY (AvnPoint p); extern CGFloat PrimaryDisplayHeight(); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index aaaf381b26c..f231ff9a71f 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -1,6 +1,7 @@ //This file will contain actual IID structures #define COM_GUIDS_MATERIALIZE #include "common.h" +#include "window.h" static bool s_generateDefaultAppMenuItems = true; static NSString* s_appTitle = @"Avalonia"; @@ -259,6 +260,12 @@ virtual HRESULT CreateMenuItemSeparator (IAvnMenuItem** ppv) override return S_OK; } + virtual HRESULT CreateAutomationNode (IAvnAutomationPeer* peer, IAvnAutomationNode** ppv) override + { + *ppv = ::CreateAutomationNode(peer); + return S_OK; + } + virtual HRESULT SetAppMenu (IAvnMenu* appMenu) override { ::SetAppMenu(s_appTitle, appMenu); @@ -295,6 +302,15 @@ NSPoint ToNSPoint (AvnPoint p) return result; } +NSRect ToNSRect (AvnRect r) +{ + return NSRect + { + NSPoint { r.X, r.Y }, + NSSize { r.Width, r.Height } + }; +} + AvnPoint ToAvnPoint (NSPoint p) { AvnPoint result; diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index b1f64bca880..200b442fcd9 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -39,6 +39,7 @@ class WindowBaseImpl; struct INSWindowHolder { virtual AvnWindow* _Nonnull GetNSWindow () = 0; + virtual AvnView* _Nonnull GetNSView () = 0; }; struct IWindowStateChanged diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 870345e5435..dc009dc5a0d 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -5,14 +5,25 @@ #include "menu.h" #include #include "rendertarget.h" +#include "AvnString.h" +#include "automation.h" -class WindowBaseImpl : public virtual ComSingleObject, public INSWindowHolder +class WindowBaseImpl : public virtual ComObject, + public virtual IAvnWindowBase, + public virtual IAvnAutomationNode, + public INSWindowHolder, + public INSAccessibilityHolder { private: NSCursor* cursor; public: FORWARD_IUNKNOWN() + BEGIN_INTERFACE_MAP() + INTERFACE_MAP_ENTRY(IAvnWindowBase, IID_IAvnWindowBase) + INTERFACE_MAP_ENTRY(IAvnAutomationNode, IID_IAvnAutomationNode) + END_INTERFACE_MAP() + virtual ~WindowBaseImpl() { View = NULL; @@ -104,6 +115,16 @@ virtual HRESULT ObtainNSViewHandleRetained(void** ret) override { return Window; } + + virtual AvnView* GetNSView() override + { + return View; + } + + virtual NSObject* GetNSAccessibility() override + { + return Window; + } virtual HRESULT Show(bool activate) override { @@ -1846,6 +1867,8 @@ @implementation AvnWindow bool _isExtended; AvnMenu* _menu; double _lastScaling; + IAvnAutomationPeer* _automationPeer; + NSMutableArray* _automationChildren; } -(void) setIsExtended:(bool)value; @@ -2218,6 +2241,64 @@ - (void)windowDidMove:(NSNotification *)notification _parent->BaseEvents->PositionChanged(position); } } + +- (BOOL)isAccessibilityElement +{ + [self getAutomationPeer]; + return YES; +} + +- (NSString *)accessibilityIdentifier +{ + auto peer = [self getAutomationPeer]; + return GetNSStringAndRelease(peer->GetAutomationId()); +} + +- (NSArray *)accessibilityChildren +{ + auto peer = [self getAutomationPeer]; + + if (_automationChildren == nullptr) + { + _automationChildren = (NSMutableArray*)[super accessibilityChildren]; + + auto childPeers = peer->GetChildren(); + auto childCount = childPeers != nullptr ? childPeers->GetCount() : 0; + + if (childCount > 0) + { + for (int i = 0; i < childCount; ++i) + { + IAvnAutomationPeer* child; + + if (childPeers->Get(i, &child) == S_OK) + { + auto element = GetAccessibilityElement(child); + [_automationChildren addObject:element]; + } + } + } + } + + return _automationChildren; +} + +- (id)accessibilityHitTest:(NSPoint)point +{ + point = [self convertPointFromScreen:point]; + auto p = [_parent->View translateLocalPoint:ToAvnPoint(point)]; + auto peer = [self getAutomationPeer]; + auto hit = peer->RootProvider_GetPeerFromPoint(p); + return GetAccessibilityElement(hit); +} + +- (IAvnAutomationPeer* _Nonnull) getAutomationPeer +{ + if (_automationPeer == nullptr) + _automationPeer = _parent->BaseEvents->AutomationStarted(_parent); + return _automationPeer; +} + @end class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup diff --git a/src/Avalonia.Native/AutomationNode.cs b/src/Avalonia.Native/AutomationNode.cs new file mode 100644 index 00000000000..c45ade520a0 --- /dev/null +++ b/src/Avalonia.Native/AutomationNode.cs @@ -0,0 +1,36 @@ +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; +using Avalonia.Native.Interop; + +#nullable enable + +namespace Avalonia.Native +{ + internal class AutomationNode : IRootAutomationNode + { + public AutomationNode(AutomationNodeFactory factory, IAvnAutomationNode native) + { + Native = native; + Factory = factory; + } + + public IAvnAutomationNode Native { get; } + public IAutomationNodeFactory Factory { get; } + + public void ChildrenChanged() + { + // TODO + } + + public void PropertyChanged(AutomationProperty property, object? oldValue, object? newValue) + { + // TODO + } + + public void FocusChanged(AutomationPeer? focus) + { + // TODO + } + } +} diff --git a/src/Avalonia.Native/AutomationNodeFactory.cs b/src/Avalonia.Native/AutomationNodeFactory.cs new file mode 100644 index 00000000000..d40009a855a --- /dev/null +++ b/src/Avalonia.Native/AutomationNodeFactory.cs @@ -0,0 +1,24 @@ +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; +using Avalonia.Native.Interop; + +namespace Avalonia.Native +{ + internal class AutomationNodeFactory : IAutomationNodeFactory + { + private static AutomationNodeFactory _instance; + private readonly IAvaloniaNativeFactory _native; + + public static AutomationNodeFactory GetInstance(IAvaloniaNativeFactory native) + { + return _instance ??= new AutomationNodeFactory(native); + } + + private AutomationNodeFactory(IAvaloniaNativeFactory native) => _native = native; + + public IAutomationNode CreateNode(AutomationPeer peer) + { + return new AutomationNode(this, _native.CreateAutomationNode(new AvnAutomationPeer(peer))); + } + } +} diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs new file mode 100644 index 00000000000..a1519d8d267 --- /dev/null +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; +using Avalonia.Native.Interop; + +#nullable enable + +namespace Avalonia.Native +{ + internal class AvnAutomationPeer : CallbackBase, IAvnAutomationPeer + { + private readonly AutomationPeer _inner; + + public AvnAutomationPeer(AutomationPeer inner) => _inner = inner; + + public IAvnAutomationNode Node => ((AutomationNode)_inner.Node).Native; + public IAvnString? AcceleratorKey => _inner.GetAcceleratorKey().ToAvnString(); + public IAvnString? AccessKey => _inner.GetAccessKey().ToAvnString(); + public AvnAutomationControlType AutomationControlType => (AvnAutomationControlType)_inner.GetAutomationControlType(); + public IAvnString? AutomationId => _inner.GetAutomationId().ToAvnString(); + public AvnRect BoundingRectangle => _inner.GetBoundingRectangle().ToAvnRect(); + public IAvnAutomationPeerArray Children => new AvnAutomationPeerArray(_inner.GetChildren()); + public IAvnString ClassName => _inner.GetClassName().ToAvnString(); + public IAvnAutomationPeer? LabeledBy => Wrap(_inner.GetLabeledBy()); + public IAvnString Name => _inner.GetName().ToAvnString(); + public IAvnAutomationPeer? Parent => Wrap(_inner.GetParent()); + + public int HasKeyboardFocus() => _inner.HasKeyboardFocus().AsComBool(); + public int IsContentElement() => _inner.IsContentElement().AsComBool(); + public int IsControlElement() => _inner.IsControlElement().AsComBool(); + public int IsEnabled() => _inner.IsEnabled().AsComBool(); + public int IsKeyboardFocusable() => _inner.IsKeyboardFocusable().AsComBool(); + public void SetFocus() => _inner.SetFocus(); + public int ShowContextMenu() => _inner.ShowContextMenu().AsComBool(); + + public IAvnAutomationPeer? RootPeer + { + get + { + var peer = _inner; + var parent = peer.GetParent(); + + while (!(peer is IRootProvider) && parent is object) + { + peer = parent; + parent = peer.GetParent(); + } + + return new AvnAutomationPeer(peer); + } + } + + public int IsRootProvider() => (_inner is IRootProvider).AsComBool(); + + public IAvnAutomationPeer? RootProvider_GetPeerFromPoint(AvnPoint point) + { + var result = ((IRootProvider)_inner).GetPeerFromPoint(point.ToAvaloniaPoint()); + + if (result is null) + return null; + + // The OSX accessibility APIs expect non-ignored elements when hit-testing. + while (!result.IsControlElement()) + { + var parent = result.GetParent(); + + if (parent is object) + result = parent; + else + break; + } + + return Wrap(result); + } + + public int IsInvokeProvider() => (_inner is IInvokeProvider).AsComBool(); + + public void InvokeProvider_Invoke() => ((IInvokeProvider)_inner).Invoke(); + + public static AvnAutomationPeer? Wrap(AutomationPeer? peer) => + peer != null ? new AvnAutomationPeer(peer) : null; + } + + internal class AvnAutomationPeerArray : CallbackBase, IAvnAutomationPeerArray + { + private readonly AvnAutomationPeer[] _items; + + public AvnAutomationPeerArray(IReadOnlyList items) + { + _items = items.Select(x => new AvnAutomationPeer(x)).ToArray(); + } + + public uint Count => (uint)_items.Length; + public IAvnAutomationPeer Get(uint index) => _items[index]; + } +} diff --git a/src/Avalonia.Native/AvnString.cs b/src/Avalonia.Native/AvnString.cs index dcd473bae32..bcaa16c5b2d 100644 --- a/src/Avalonia.Native/AvnString.cs +++ b/src/Avalonia.Native/AvnString.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using System.Text; namespace Avalonia.Native.Interop { @@ -13,6 +14,53 @@ partial interface IAvnStringArray { string[] ToStringArray(); } + + internal class AvnString : CallbackBase, IAvnString + { + private IntPtr _native; + private int _nativeLen; + + public AvnString(string s) => String = s; + + public string String { get; } + public byte[] Bytes => Encoding.UTF8.GetBytes(String); + + public unsafe void* Pointer() + { + EnsureNative(); + return _native.ToPointer(); + } + + public int Length() + { + EnsureNative(); + return _nativeLen; + } + + protected override void Destroyed() + { + if (_native != IntPtr.Zero) + { + Marshal.FreeHGlobal(_native); + _native = IntPtr.Zero; + } + } + + private unsafe void EnsureNative() + { + if (string.IsNullOrEmpty(String)) + return; + if (_native == IntPtr.Zero) + { + _nativeLen = Encoding.UTF8.GetByteCount(String); + _native = Marshal.AllocHGlobal(_nativeLen + 1); + var ptr = (byte*)_native.ToPointer(); + fixed (char* chars = String) + Encoding.UTF8.GetBytes(chars, String.Length, ptr, _nativeLen); + ptr[_nativeLen] = 0; + } + } + } } namespace Avalonia.Native.Interop.Impl { diff --git a/src/Avalonia.Native/Helpers.cs b/src/Avalonia.Native/Helpers.cs index 564434a04cd..764ff789dcb 100644 --- a/src/Avalonia.Native/Helpers.cs +++ b/src/Avalonia.Native/Helpers.cs @@ -1,4 +1,5 @@ using Avalonia.Native.Interop; +using JetBrains.Annotations; namespace Avalonia.Native { @@ -24,11 +25,21 @@ public static AvnPoint ToAvnPoint(this PixelPoint pt) return new AvnPoint { X = pt.X, Y = pt.Y }; } + public static AvnRect ToAvnRect (this Rect rect) + { + return new AvnRect() { X = rect.X, Y= rect.Y, Height = rect.Height, Width = rect.Width }; + } + public static AvnSize ToAvnSize (this Size size) { return new AvnSize { Height = size.Height, Width = size.Width }; } + public static IAvnString ToAvnString(this string s) + { + return s != null ? new AvnString(s) : null; + } + public static Size ToAvaloniaSize (this AvnSize size) { return new Size(size.Width, size.Height); diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index c36675afcde..83f2cc2f826 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -7,7 +7,6 @@ namespace Avalonia.Native { class PopupImpl : WindowBaseImpl, IPopupImpl { - private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; private readonly AvaloniaNativePlatformOpenGlInterface _glFeature; private readonly IWindowBaseImpl _parent; @@ -15,9 +14,8 @@ class PopupImpl : WindowBaseImpl, IPopupImpl public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, AvaloniaNativePlatformOpenGlInterface glFeature, - IWindowBaseImpl parent) : base(opts, glFeature) + IWindowBaseImpl parent) : base(factory, opts, glFeature) { - _factory = factory; _opts = opts; _glFeature = glFeature; _parent = parent; diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index f3b60f07be2..05cfb5b2ed5 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -13,7 +13,6 @@ namespace Avalonia.Native { internal class WindowImpl : WindowBaseImpl, IWindowImpl, ITopLevelImplWithNativeMenuExporter { - private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; private readonly AvaloniaNativePlatformOpenGlInterface _glFeature; IAvnWindow _native; @@ -22,9 +21,8 @@ internal class WindowImpl : WindowBaseImpl, IWindowImpl, ITopLevelImplWithNative internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - AvaloniaNativePlatformOpenGlInterface glFeature) : base(opts, glFeature) + AvaloniaNativePlatformOpenGlInterface glFeature) : base(factory, opts, glFeature) { - _factory = factory; _opts = opts; _glFeature = glFeature; _doubleClickHelper = new DoubleClickHelper(); diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index b45a0787b90..1e0d7ca0f4a 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -48,6 +48,7 @@ public IntPtr GetNSWindowRetained() internal abstract class WindowBaseImpl : IWindowBaseImpl, IFramebufferPlatformSurface, ITopLevelImplWithNativeControlHost { + protected readonly IAvaloniaNativeFactory _factory; protected IInputRoot _inputRoot; IAvnWindowBase _native; private object _syncRoot = new object(); @@ -63,8 +64,10 @@ internal abstract class WindowBaseImpl : IWindowBaseImpl, private NativeControlHostImpl _nativeControlHost; private IGlContext _glContext; - internal WindowBaseImpl(AvaloniaNativePlatformOptions opts, AvaloniaNativePlatformOpenGlInterface glFeature) + internal WindowBaseImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, + AvaloniaNativePlatformOpenGlInterface glFeature) { + _factory = factory; _gpu = opts.UseGpu && glFeature != null; _deferredRendering = opts.UseDeferredRendering; @@ -92,6 +95,8 @@ protected void Init(IAvnWindowBase window, IAvnScreens screens, IGlContext glCon Resize(new Size(monitor.WorkingArea.Width * 0.75d, monitor.WorkingArea.Height * 0.7d)); } + public IAvnWindowBase Native => _native; + public Size ClientSize { get @@ -244,8 +249,14 @@ public AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, return (AvnDragDropEffects)args.Effects; } } - } + public IAvnAutomationPeer AutomationStarted(IAvnAutomationNode node) + { + var factory = AutomationNodeFactory.GetInstance(_parent._factory); + return new AvnAutomationPeer(_parent.AutomationStarted(new AutomationNode(factory, node))); + } + } + public void Activate() { _native.Activate(); diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index adcbeb2d3ac..8357742f550 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -400,6 +400,49 @@ enum AvnExtendClientAreaChromeHints AvnDefaultChrome = AvnPreferSystemChrome, } +enum AvnAutomationControlType +{ + AutomationButton, + AutomationCalendar, + AutomationCheckBox, + AutomationComboBox, + AutomationEdit, + AutomationHyperlink, + AutomationImage, + AutomationListItem, + AutomationList, + AutomationMenu, + AutomationMenuBar, + AutomationMenuItem, + AutomationProgressBar, + AutomationRadioButton, + AutomationScrollBar, + AutomationSlider, + AutomationSpinner, + AutomationStatusBar, + AutomationTab, + AutomationTabItem, + AutomationText, + AutomationToolBar, + AutomationToolTip, + AutomationTree, + AutomationTreeItem, + AutomationCustom, + AutomationGroup, + AutomationThumb, + AutomationDataGrid, + AutomationDataItem, + AutomationDocument, + AutomationSplitButton, + AutomationWindow, + AutomationPane, + AutomationHeader, + AutomationHeaderItem, + AutomationTable, + AutomationTitleBar, + AutomationSeparator, +} + [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)] interface IAvaloniaNativeFactory : IUnknown { @@ -418,6 +461,7 @@ interface IAvaloniaNativeFactory : IUnknown HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv); HRESULT CreateMenuItem(IAvnMenuItem** ppv); HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv); + HRESULT CreateAutomationNode(IAvnAutomationPeer* peer, IAvnAutomationNode** ppv); } [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)] @@ -506,6 +550,7 @@ interface IAvnWindowBaseEvents : IUnknown AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, AvnInputModifiers modifiers, AvnDragDropEffects effects, IAvnClipboard* clipboard, [intptr]void* dataObjectHandle); + IAvnAutomationPeer* AutomationStarted(IAvnAutomationNode* node); } [uuid(1ae178ee-1fcc-447f-b6dd-b7bb727f934c)] @@ -733,3 +778,46 @@ interface IAvnApplicationEvents : IUnknown { void FilesOpened (IAvnStringArray* urls); } + +[uuid(b87016f3-7eec-41de-b385-07844c268dc4)] +interface IAvnAutomationPeer : IUnknown +{ + IAvnAutomationNode* GetNode(); + IAvnString* GetAcceleratorKey(); + IAvnString* GetAccessKey(); + AvnAutomationControlType GetAutomationControlType(); + IAvnString* GetAutomationId(); + AvnRect GetBoundingRectangle(); + IAvnAutomationPeerArray* GetChildren(); + IAvnString* GetClassName(); + IAvnAutomationPeer* GetLabeledBy(); + IAvnString* GetName(); + IAvnAutomationPeer* GetParent(); + bool HasKeyboardFocus(); + bool IsContentElement(); + bool IsControlElement(); + bool IsEnabled(); + bool IsKeyboardFocusable(); + void SetFocus(); + bool ShowContextMenu(); + + IAvnAutomationPeer* GetRootPeer(); + + bool IsRootProvider(); + IAvnAutomationPeer* RootProvider_GetPeerFromPoint(AvnPoint point); + + bool IsInvokeProvider(); + void InvokeProvider_Invoke(); +} + +[uuid(b00af5da-78af-4b33-bfff-4ce13a6239a9)] +interface IAvnAutomationPeerArray : IUnknown +{ + uint GetCount(); + HRESULT Get(uint index, IAvnAutomationPeer**ppv); +} + +[uuid(004dc40b-e435-49dc-bac5-6272ee35382a)] +interface IAvnAutomationNode : IUnknown +{ +} From bcc8876e8e434210b3c725ce88a6525ed9ab359f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 26 Mar 2021 22:16:30 +0100 Subject: [PATCH 17/73] Handle children changing. And map all the automation control types to roles. --- native/Avalonia.Native/src/OSX/automation.mm | 68 ++++++++++++++------ native/Avalonia.Native/src/OSX/window.mm | 5 ++ src/Avalonia.Native/AutomationNode.cs | 5 +- src/Avalonia.Native/avn.idl | 1 + 4 files changed, 54 insertions(+), 25 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index 064b4140941..32230ff5b79 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -21,6 +21,11 @@ _node = [[AvnAutomationNode alloc] initWithPeer: peer]; } + virtual void ChildrenChanged() override + { + NSAccessibilityPostNotification(_node, NSAccessibilityLayoutChangedNotification); + } + virtual NSObject* GetNSAccessibility() override { return _node; @@ -50,25 +55,46 @@ - (NSAccessibilityRole)accessibilityRole auto controlType = _peer->GetAutomationControlType(); switch (controlType) { - case AutomationButton: - return NSAccessibilityButtonRole; - case AutomationCheckBox: - return NSAccessibilityCheckBoxRole; - case AutomationComboBox: - return NSAccessibilityPopUpButtonRole; - case AutomationGroup: - case AutomationPane: - return NSAccessibilityGroupRole; - case AutomationSlider: - return NSAccessibilitySliderRole; - case AutomationTab: - return NSAccessibilityTabGroupRole; - case AutomationTabItem: - return NSAccessibilityRadioButtonRole; - case AutomationWindow: - return NSAccessibilityWindowRole; - default: - return NSAccessibilityUnknownRole; + case AutomationButton: return NSAccessibilityButtonRole; + case AutomationCalendar: return NSAccessibilityGridRole; + case AutomationCheckBox: return NSAccessibilityCheckBoxRole; + case AutomationComboBox: return NSAccessibilityPopUpButtonRole; + case AutomationEdit: return NSAccessibilityTextFieldRole; + case AutomationHyperlink: return NSAccessibilityLinkRole; + case AutomationImage: return NSAccessibilityImageRole; + case AutomationListItem: return NSAccessibilityRowRole; + case AutomationList: return NSAccessibilityTableRole; + case AutomationMenu: return NSAccessibilityMenuBarRole; + case AutomationMenuBar: return NSAccessibilityMenuBarRole; + case AutomationMenuItem: return NSAccessibilityMenuItemRole; + case AutomationProgressBar: return NSAccessibilityProgressIndicatorRole; + case AutomationRadioButton: return NSAccessibilityRadioButtonRole; + case AutomationScrollBar: return NSAccessibilityScrollBarRole; + case AutomationSlider: return NSAccessibilitySliderRole; + case AutomationSpinner: return NSAccessibilityIncrementorRole; + case AutomationStatusBar: return NSAccessibilityTableRole; + case AutomationTab: return NSAccessibilityTabGroupRole; + case AutomationTabItem: return NSAccessibilityRadioButtonRole; + case AutomationText: return NSAccessibilityTextFieldRole; + case AutomationToolBar: return NSAccessibilityToolbarRole; + case AutomationToolTip: return NSAccessibilityPopoverRole; + case AutomationTree: return NSAccessibilityOutlineRole; + case AutomationTreeItem: return NSAccessibilityOutlineRowSubrole; + case AutomationCustom: return NSAccessibilityUnknownRole; + case AutomationGroup: return NSAccessibilityGroupRole; + case AutomationThumb: return NSAccessibilityHandleRole; + case AutomationDataGrid: return NSAccessibilityGridRole; + case AutomationDataItem: return NSAccessibilityCellRole; + case AutomationDocument: return NSAccessibilityStaticTextRole; + case AutomationSplitButton: return NSAccessibilityPopUpButtonRole; + case AutomationWindow: return NSAccessibilityWindowRole; + case AutomationPane: return NSAccessibilityGroupRole; + case AutomationHeader: return NSAccessibilityGroupRole; + case AutomationHeaderItem: return NSAccessibilityButtonRole; + case AutomationTable: return NSAccessibilityTableRole; + case AutomationTitleBar: return NSAccessibilityGroupRole; + case AutomationSeparator: return NSAccessibilityUnknownRole; + default: return NSAccessibilityUnknownRole; } } @@ -188,13 +214,13 @@ - (IAvnWindowBase*)getWindow - (AvnWindow*) getAvnWindow { auto window = [self getWindow]; - return dynamic_cast(window)->GetNSWindow(); + return window != nullptr ? dynamic_cast(window)->GetNSWindow() : nullptr; } - (AvnView*) getAvnView { auto window = [self getWindow]; - return dynamic_cast(window)->GetNSView(); + return window != nullptr ? dynamic_cast(window)->GetNSView() : nullptr; } @end diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index dc009dc5a0d..456a2db3ee7 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -509,6 +509,11 @@ virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint p return S_OK; } + virtual void ChildrenChanged() override + { + NSAccessibilityPostNotification(Window, NSAccessibilityLayoutChangedNotification); + } + protected: virtual NSWindowStyleMask GetStyle() { diff --git a/src/Avalonia.Native/AutomationNode.cs b/src/Avalonia.Native/AutomationNode.cs index c45ade520a0..c0e46b52b9c 100644 --- a/src/Avalonia.Native/AutomationNode.cs +++ b/src/Avalonia.Native/AutomationNode.cs @@ -18,10 +18,7 @@ public AutomationNode(AutomationNodeFactory factory, IAvnAutomationNode native) public IAvnAutomationNode Native { get; } public IAutomationNodeFactory Factory { get; } - public void ChildrenChanged() - { - // TODO - } + public void ChildrenChanged() => Native.ChildrenChanged(); public void PropertyChanged(AutomationProperty property, object? oldValue, object? newValue) { diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 8357742f550..5697665545e 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -820,4 +820,5 @@ interface IAvnAutomationPeerArray : IUnknown [uuid(004dc40b-e435-49dc-bac5-6272ee35382a)] interface IAvnAutomationNode : IUnknown { + void ChildrenChanged(); } From 7bbbfa414f3407ffc7710230613829bc35dabef2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 27 Mar 2021 00:07:15 +0100 Subject: [PATCH 18/73] More OSX accessibility implementation. --- native/Avalonia.Native/src/OSX/automation.mm | 74 ++++++++++++++++++- native/Avalonia.Native/src/OSX/window.mm | 4 + .../Peers/RangeBaseAutomationPeer.cs | 3 + .../Provider/IRangeValueProvider.cs | 12 +++ src/Avalonia.Native/AutomationNode.cs | 15 +++- src/Avalonia.Native/AvnAutomationPeer.cs | 15 +++- src/Avalonia.Native/avn.idl | 23 ++++++ 7 files changed, 142 insertions(+), 4 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index 32230ff5b79..b400785a134 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -26,6 +26,17 @@ virtual void ChildrenChanged() override NSAccessibilityPostNotification(_node, NSAccessibilityLayoutChangedNotification); } + virtual void PropertyChanged(AvnAutomationProperty property) override + { + switch (property) { + case RangeValueProvider_Value: + NSAccessibilityPostNotification(_node, NSAccessibilityValueChangedNotification); + break; + default: + break; + } + } + virtual NSObject* GetNSAccessibility() override { return _node; @@ -75,7 +86,7 @@ - (NSAccessibilityRole)accessibilityRole case AutomationStatusBar: return NSAccessibilityTableRole; case AutomationTab: return NSAccessibilityTabGroupRole; case AutomationTabItem: return NSAccessibilityRadioButtonRole; - case AutomationText: return NSAccessibilityTextFieldRole; + case AutomationText: return NSAccessibilityStaticTextRole; case AutomationToolBar: return NSAccessibilityToolbarRole; case AutomationToolTip: return NSAccessibilityPopoverRole; case AutomationTree: return NSAccessibilityOutlineRole; @@ -105,7 +116,39 @@ - (NSString *)accessibilityIdentifier - (NSString *)accessibilityTitle { - return GetNSStringAndRelease(_peer->GetName()); + // StaticText exposes its text via the value property. + if (_peer->GetAutomationControlType() != AutomationText) + { + return GetNSStringAndRelease(_peer->GetName()); + } + + return [super accessibilityTitle]; +} + +- (id)accessibilityValue +{ + if (_peer->IsRangeValueProvider()) + { + return [NSNumber numberWithDouble:_peer->RangeValueProvider_GetValue()]; + } + else if (_peer->IsToggleProvider()) + { + switch (_peer->ToggleProvider_GetToggleState()) { + case 0: return [NSNumber numberWithBool:NO]; + case 1: return [NSNumber numberWithBool:YES]; + default: return [NSNumber numberWithInt:-1]; + } + } + else if (_peer->IsValueProvider()) + { + return GetNSStringAndRelease(_peer->ValueProvider_GetValue()); + } + else if (_peer->GetAutomationControlType() == AutomationText) + { + return GetNSStringAndRelease(_peer->GetName()); + } + + return [super accessibilityValue]; } - (NSArray *)accessibilityChildren @@ -175,16 +218,43 @@ - (id)accessibilityWindow - (BOOL)accessibilityPerformPress { + if (!_peer->IsInvokeProvider()) + return NO; _peer->InvokeProvider_Invoke(); return YES; } +- (BOOL)accessibilityPerformIncrement +{ + if (!_peer->IsRangeValueProvider()) + return NO; + auto value = _peer->RangeValueProvider_GetValue(); + value += _peer->RangeValueProvider_GetSmallChange(); + _peer->RangeValueProvider_SetValue(value); + return YES; +} + +- (BOOL)accessibilityPerformDecrement +{ + if (!_peer->IsRangeValueProvider()) + return NO; + auto value = _peer->RangeValueProvider_GetValue(); + value -= _peer->RangeValueProvider_GetSmallChange(); + _peer->RangeValueProvider_SetValue(value); + return YES; +} + - (BOOL)isAccessibilitySelectorAllowed:(SEL)selector { if (selector == @selector(accessibilityPerformPress)) { return _peer->IsInvokeProvider(); } + else if (selector == @selector(accessibilityPerformIncrement) || + selector == @selector(accessibilityPerformDecrement)) + { + return _peer->IsRangeValueProvider(); + } return [super isAccessibilitySelectorAllowed:selector]; } diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 456a2db3ee7..5d69fc8e75f 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -514,6 +514,10 @@ virtual void ChildrenChanged() override NSAccessibilityPostNotification(Window, NSAccessibilityLayoutChangedNotification); } + virtual void PropertyChanged(AvnAutomationProperty property) override + { + } + protected: virtual NSWindowStyleMask GetStyle() { diff --git a/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs index bafc4c14fc9..31a2b7e7af7 100644 --- a/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs @@ -19,6 +19,9 @@ public RangeBaseAutomationPeer(IAutomationNodeFactory factory, RangeBase owner) public double Maximum => Owner.Maximum; public double Minimum => Owner.Minimum; public double Value => Owner.Value; + public double SmallChange => Owner.SmallChange; + public double LargeChange => Owner.LargeChange; + public void SetValue(double value) => Owner.Value = value; protected virtual void OwnerPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) diff --git a/src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs b/src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs index d4cd35fcf9b..d494e068f73 100644 --- a/src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs +++ b/src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs @@ -28,6 +28,18 @@ public interface IRangeValueProvider /// double Value { get; } + /// + /// Gets the value that is added to or subtracted from the Value property when a large + /// change is made, such as with the PAGE DOWN key. + /// + double LargeChange { get; } + + /// + /// Gets the value that is added to or subtracted from the Value property when a small + /// change is made, such as with an arrow key. + /// + double SmallChange { get; } + /// /// Sets the value of the control. /// diff --git a/src/Avalonia.Native/AutomationNode.cs b/src/Avalonia.Native/AutomationNode.cs index c0e46b52b9c..2601b2f2395 100644 --- a/src/Avalonia.Native/AutomationNode.cs +++ b/src/Avalonia.Native/AutomationNode.cs @@ -22,7 +22,20 @@ public AutomationNode(AutomationNodeFactory factory, IAvnAutomationNode native) public void PropertyChanged(AutomationProperty property, object? oldValue, object? newValue) { - // TODO + AvnAutomationProperty p; + + if (property == AutomationElementIdentifiers.BoundingRectangleProperty) + p = AvnAutomationProperty.AutomationPeer_BoundingRectangle; + else if (property == AutomationElementIdentifiers.ClassNameProperty) + p = AvnAutomationProperty.AutomationPeer_ClassName; + else if (property == AutomationElementIdentifiers.NameProperty) + p = AvnAutomationProperty.AutomationPeer_Name; + else if (property == RangeValuePatternIdentifiers.ValueProperty) + p = AvnAutomationProperty.RangeValueProvider_Value; + else + return; + + Native.PropertyChanged(p); } public void FocusChanged(AutomationPeer? focus) diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index a1519d8d267..0c4ad58b9d2 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -75,8 +75,21 @@ public IAvnAutomationPeer? RootPeer } public int IsInvokeProvider() => (_inner is IInvokeProvider).AsComBool(); - public void InvokeProvider_Invoke() => ((IInvokeProvider)_inner).Invoke(); + + public int IsRangeValueProvider() => (_inner is IRangeValueProvider).AsComBool(); + public double RangeValueProvider_GetValue() => ((IRangeValueProvider)_inner).Value; + public double RangeValueProvider_GetSmallChange() => ((IRangeValueProvider)_inner).SmallChange; + public double RangeValueProvider_GetLargeChange() => ((IRangeValueProvider)_inner).LargeChange; + public void RangeValueProvider_SetValue(double value) => ((IRangeValueProvider)_inner).SetValue(value); + + public int IsToggleProvider() => (_inner is IToggleProvider).AsComBool(); + public int ToggleProvider_GetToggleState() => (int)((IToggleProvider)_inner).ToggleState; + public void ToggleProvider_Toggle() => ((IToggleProvider)_inner).Toggle(); + + public int IsValueProvider() => (_inner is IValueProvider).AsComBool(); + public IAvnString ValueProvider_GetValue() => ((IValueProvider)_inner).Value.ToAvnString(); + public void ValueProvider_SetValue(string value) => ((IValueProvider)_inner).SetValue(value); public static AvnAutomationPeer? Wrap(AutomationPeer? peer) => peer != null ? new AvnAutomationPeer(peer) : null; diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 5697665545e..88c92254195 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -218,6 +218,14 @@ enum SystemDecorations { SystemDecorationsFull = 2, } +enum AvnAutomationProperty +{ + AutomationPeer_BoundingRectangle, + AutomationPeer_ClassName, + AutomationPeer_Name, + RangeValueProvider_Value, +} + struct AvnSize { double Width, Height; @@ -808,6 +816,20 @@ interface IAvnAutomationPeer : IUnknown bool IsInvokeProvider(); void InvokeProvider_Invoke(); + + bool IsRangeValueProvider(); + double RangeValueProvider_GetValue(); + double RangeValueProvider_GetSmallChange(); + double RangeValueProvider_GetLargeChange(); + void RangeValueProvider_SetValue(double value); + + bool IsToggleProvider(); + int ToggleProvider_GetToggleState(); + void ToggleProvider_Toggle(); + + bool IsValueProvider(); + IAvnString* ValueProvider_GetValue(); + void ValueProvider_SetValue(char* value); } [uuid(b00af5da-78af-4b33-bfff-4ce13a6239a9)] @@ -821,4 +843,5 @@ interface IAvnAutomationPeerArray : IUnknown interface IAvnAutomationNode : IUnknown { void ChildrenChanged(); + void PropertyChanged(AvnAutomationProperty property); } From 0ab5e4dbfecc424397c27fbcca8683c7d4a949ea Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 27 Mar 2021 14:40:59 +0100 Subject: [PATCH 19/73] Implemented a11y focus on OSX. --- native/Avalonia.Native/src/OSX/automation.mm | 7 ++++++- native/Avalonia.Native/src/OSX/window.mm | 17 +++++++++++++++++ src/Avalonia.Native/AutomationNode.cs | 5 +---- src/Avalonia.Native/AvnAutomationPeer.cs | 1 + src/Avalonia.Native/avn.idl | 2 ++ 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index b400785a134..ff72cc2a202 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -36,7 +36,12 @@ virtual void PropertyChanged(AvnAutomationProperty property) override break; } } - + + virtual void FocusChanged(IAvnAutomationPeer* peer) override + { + // Only implemented in top-level nodes, i.e. AvnWindow. + } + virtual NSObject* GetNSAccessibility() override { return _node; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 5d69fc8e75f..62ca8e32dfe 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -518,6 +518,11 @@ virtual void PropertyChanged(AvnAutomationProperty property) override { } + virtual void FocusChanged(IAvnAutomationPeer* peer) override + { + NSAccessibilityPostNotification(Window, NSAccessibilityFocusedUIElementChangedNotification); + } + protected: virtual NSWindowStyleMask GetStyle() { @@ -2301,6 +2306,18 @@ - (id)accessibilityHitTest:(NSPoint)point return GetAccessibilityElement(hit); } +- (id)accessibilityFocusedUIElement +{ + auto peer = [self getAutomationPeer]; + + if (peer->IsRootProvider()) + { + return GetAccessibilityElement(peer->RootProvider_GetFocus()); + } + + return [super accessibilityFocusedUIElement]; +} + - (IAvnAutomationPeer* _Nonnull) getAutomationPeer { if (_automationPeer == nullptr) diff --git a/src/Avalonia.Native/AutomationNode.cs b/src/Avalonia.Native/AutomationNode.cs index 2601b2f2395..f85813a6fbd 100644 --- a/src/Avalonia.Native/AutomationNode.cs +++ b/src/Avalonia.Native/AutomationNode.cs @@ -38,9 +38,6 @@ public void PropertyChanged(AutomationProperty property, object? oldValue, objec Native.PropertyChanged(p); } - public void FocusChanged(AutomationPeer? focus) - { - // TODO - } + public void FocusChanged(AutomationPeer? focus) => Native.FocusChanged(AvnAutomationPeer.Wrap(focus)); } } diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index 0c4ad58b9d2..fb3759b77e4 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -52,6 +52,7 @@ public IAvnAutomationPeer? RootPeer } public int IsRootProvider() => (_inner is IRootProvider).AsComBool(); + public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(((IRootProvider)_inner).GetFocus()); public IAvnAutomationPeer? RootProvider_GetPeerFromPoint(AvnPoint point) { diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 88c92254195..369031b72c3 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -812,6 +812,7 @@ interface IAvnAutomationPeer : IUnknown IAvnAutomationPeer* GetRootPeer(); bool IsRootProvider(); + IAvnAutomationPeer* RootProvider_GetFocus(); IAvnAutomationPeer* RootProvider_GetPeerFromPoint(AvnPoint point); bool IsInvokeProvider(); @@ -844,4 +845,5 @@ interface IAvnAutomationNode : IUnknown { void ChildrenChanged(); void PropertyChanged(AvnAutomationProperty property); + void FocusChanged(IAvnAutomationPeer* peer); } From 1553b89bd78d6766a3a141b5fb23a22d4814e98b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 28 Mar 2021 00:09:26 +0100 Subject: [PATCH 20/73] Fix a11y for popup windows. - Don't show when creating a popup window with a11y enabled - Raise focus changed notification on element that got focus instead of the window --- native/Avalonia.Native/src/OSX/window.mm | 9 +++-- src/Avalonia.Native/WindowImplBase.cs | 44 ++++++++++++++++++++---- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 62ca8e32dfe..ba563c051ad 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -520,7 +520,12 @@ virtual void PropertyChanged(AvnAutomationProperty property) override virtual void FocusChanged(IAvnAutomationPeer* peer) override { - NSAccessibilityPostNotification(Window, NSAccessibilityFocusedUIElementChangedNotification); + auto element = GetAccessibilityElement(peer); + + if (element != nullptr) + { + NSAccessibilityPostNotification(element, NSAccessibilityFocusedUIElementChangedNotification); + } } protected: @@ -2318,7 +2323,7 @@ - (id)accessibilityFocusedUIElement return [super accessibilityFocusedUIElement]; } -- (IAvnAutomationPeer* _Nonnull) getAutomationPeer +- (IAvnAutomationPeer*) getAutomationPeer { if (_automationPeer == nullptr) _automationPeer = _parent->BaseEvents->AutomationStarted(_parent); diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 1e0d7ca0f4a..92576578faa 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -63,6 +63,9 @@ internal abstract class WindowBaseImpl : IWindowBaseImpl, private GlPlatformSurface _glSurface; private NativeControlHostImpl _nativeControlHost; private IGlContext _glContext; + private IAutomationNode _automationNode; + private AvnAutomationPeer _automationPeer; + private Func _automationStarted; internal WindowBaseImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, AvaloniaNativePlatformOpenGlInterface glFeature) @@ -209,7 +212,6 @@ int IAvnWindowBaseEvents.RawTextInputEvent(uint timeStamp, string text) return _parent.RawTextInputEvent(timeStamp, text).AsComBool(); } - void IAvnWindowBaseEvents.ScalingChanged(double scaling) { _parent._savedScaling = scaling; @@ -250,11 +252,7 @@ public AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, } } - public IAvnAutomationPeer AutomationStarted(IAvnAutomationNode node) - { - var factory = AutomationNodeFactory.GetInstance(_parent._factory); - return new AvnAutomationPeer(_parent.AutomationStarted(new AutomationNode(factory, node))); - } + public IAvnAutomationPeer AutomationStarted(IAvnAutomationNode node) => _parent.HandleAutomationStarted(node); } public void Activate() @@ -477,6 +475,38 @@ public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0, 0); public IPlatformHandle Handle { get; private set; } - public Func AutomationStarted { get; set; } + + public Func AutomationStarted + { + get => _automationStarted; + set + { + _automationStarted = value; + + // We've already received an AutomationStarted event, but the Window/PopupRoot wasn't initialized. + // Now it is, so notify it and store the automation peer for the next time the OS invokes an a11y + // query. + if (value is object && _automationNode is object) + _automationPeer = new AvnAutomationPeer(_automationStarted.Invoke(_automationNode)); + } + } + + private AvnAutomationPeer HandleAutomationStarted(IAvnAutomationNode node) + { + if (_automationPeer is object) + return _automationPeer; + + var factory = AutomationNodeFactory.GetInstance(_factory); + _automationNode = new AutomationNode(factory, node); + + // If automation is started during platform window creation we don't yet have a Window/PopupRoot + // control to notify. In this case we'll notify them when AutomationStarted gets set. We can safely + // return null here because the peer isn't actually needed at this point and will be re-queried the next + // time it's needed. + if (AutomationStarted is null) + return null; + + return _automationPeer = new AvnAutomationPeer(AutomationStarted(_automationNode)); + } } } From 6096cab3b97c4faa3b288832871bff9f4a9fd5cd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 28 Mar 2021 00:47:27 +0100 Subject: [PATCH 21/73] Use correct role for combobox items on OSX. --- native/Avalonia.Native/src/OSX/automation.mm | 1 + .../Automation/Peers/AutomationPeer.cs | 1 + .../Peers/ComboBoxItemAutomationPeer.cs | 24 +++++++++++++++++++ src/Avalonia.Controls/ComboBoxItem.cs | 7 ++++++ src/Avalonia.Native/avn.idl | 1 + .../Automation/AutomationNode.cs | 1 + 6 files changed, 35 insertions(+) create mode 100644 src/Avalonia.Controls/Automation/Peers/ComboBoxItemAutomationPeer.cs diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index ff72cc2a202..cff142a912a 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -75,6 +75,7 @@ - (NSAccessibilityRole)accessibilityRole case AutomationCalendar: return NSAccessibilityGridRole; case AutomationCheckBox: return NSAccessibilityCheckBoxRole; case AutomationComboBox: return NSAccessibilityPopUpButtonRole; + case AutomationComboBoxItem: return NSAccessibilityMenuItemRole; case AutomationEdit: return NSAccessibilityTextFieldRole; case AutomationHyperlink: return NSAccessibilityLinkRole; case AutomationImage: return NSAccessibilityImageRole; diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index 25af3400147..85a0e665b2f 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -13,6 +13,7 @@ public enum AutomationControlType Calendar, CheckBox, ComboBox, + ComboBoxItem, Edit, Hyperlink, Image, diff --git a/src/Avalonia.Controls/Automation/Peers/ComboBoxItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ComboBoxItemAutomationPeer.cs new file mode 100644 index 00000000000..06141ae8351 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ComboBoxItemAutomationPeer.cs @@ -0,0 +1,24 @@ +using System; +using Avalonia.Automation.Platform; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Selection; + +#nullable enable + +namespace Avalonia.Automation.Peers +{ + public class ComboBoxItemAutomationPeer : ListItemAutomationPeer + { + public ComboBoxItemAutomationPeer(IAutomationNodeFactory factory, ComboBoxItem owner) + : base(factory, owner) + { + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.ComboBoxItem; + } + } +} diff --git a/src/Avalonia.Controls/ComboBoxItem.cs b/src/Avalonia.Controls/ComboBoxItem.cs index a0a1f2a4aaa..0d5846f2707 100644 --- a/src/Avalonia.Controls/ComboBoxItem.cs +++ b/src/Avalonia.Controls/ComboBoxItem.cs @@ -1,5 +1,7 @@ using System; using System.Reactive.Linq; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Platform; namespace Avalonia.Controls { @@ -13,5 +15,10 @@ public ComboBoxItem() this.GetObservable(ComboBoxItem.IsFocusedProperty).Where(focused => focused) .Subscribe(_ => (Parent as ComboBox)?.ItemFocused(this)); } + + protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + { + return new ComboBoxItemAutomationPeer(factory, this); + } } } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 369031b72c3..9b74eef6e21 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -414,6 +414,7 @@ enum AvnAutomationControlType AutomationCalendar, AutomationCheckBox, AutomationComboBox, + AutomationComboBoxItem, AutomationEdit, AutomationHyperlink, AutomationImage, diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index 3963d0cb6cc..79ef5bc7193 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -299,6 +299,7 @@ private static UiaControlTypeId ToUiaControlType(AutomationControlType role) AutomationControlType.Calendar => UiaControlTypeId.Calendar, AutomationControlType.CheckBox => UiaControlTypeId.CheckBox, AutomationControlType.ComboBox => UiaControlTypeId.ComboBox, + AutomationControlType.ComboBoxItem => UiaControlTypeId.ListItem, AutomationControlType.Edit => UiaControlTypeId.Edit, AutomationControlType.Hyperlink => UiaControlTypeId.Hyperlink, AutomationControlType.Image => UiaControlTypeId.Image, From 8ded356614b6bef5d1c69a00da978efbacd795b1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 30 Mar 2021 14:59:54 +0200 Subject: [PATCH 22/73] More work on ComboBox OSX a11y. Implement "Show Menu" and "Perform Press" actions. --- native/Avalonia.Native/src/OSX/automation.mm | 48 +++++++++++++++++-- .../Peers/ComboBoxAutomationPeer.cs | 1 + .../Provider/IExpandCollapseProvider.cs | 9 ++++ src/Avalonia.Native/AvnAutomationPeer.cs | 14 ++++++ src/Avalonia.Native/avn.idl | 6 +++ 5 files changed, 73 insertions(+), 5 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index cff142a912a..c3ca182b912 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -222,11 +222,33 @@ - (id)accessibilityWindow return [self accessibilityTopLevelUIElement]; } -- (BOOL)accessibilityPerformPress +- (BOOL)isAccessibilityExpanded { - if (!_peer->IsInvokeProvider()) + if (!_peer->IsExpandCollapseProvider()) return NO; - _peer->InvokeProvider_Invoke(); + return _peer->ExpandCollapseProvider_IsExpanded(); +} + +- (void)setAccessibilityExpanded:(BOOL)accessibilityExpanded +{ + if (!_peer->IsExpandCollapseProvider()) + return; + if (accessibilityExpanded) + _peer->ExpandCollapseProvider_Expand(); + else + _peer->ExpandCollapseProvider_Collapse(); +} + +- (BOOL)accessibilityPerformPress +{ + if (_peer->IsInvokeProvider()) + { + _peer->InvokeProvider_Invoke(); + } + else if (_peer->IsExpandCollapseProvider()) + { + _peer->ExpandCollapseProvider_Expand(); + } return YES; } @@ -250,11 +272,27 @@ - (BOOL)accessibilityPerformDecrement return YES; } +- (BOOL)accessibilityPerformShowMenu +{ + if (!_peer->IsExpandCollapseProvider()) + return NO; + _peer->ExpandCollapseProvider_Expand(); + return YES; +} + - (BOOL)isAccessibilitySelectorAllowed:(SEL)selector { - if (selector == @selector(accessibilityPerformPress)) + if (selector == @selector(accessibilityPerformShowMenu)) + { + return _peer->IsExpandCollapseProvider() && _peer->ExpandCollapseProvider_ShowsMenu(); + } + else if (selector == @selector(isAccessibilityExpanded)) + { + return _peer->IsExpandCollapseProvider(); + } + else if (selector == @selector(accessibilityPerformPress)) { - return _peer->IsInvokeProvider(); + return _peer->IsInvokeProvider() || _peer->IsExpandCollapseProvider(); } else if (selector == @selector(accessibilityPerformIncrement) || selector == @selector(accessibilityPerformDecrement)) diff --git a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs index d11946b27e0..d8225dbc40f 100644 --- a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs @@ -20,6 +20,7 @@ public ComboBoxAutomationPeer(IAutomationNodeFactory factory, ComboBox owner) public new ComboBox Owner => (ComboBox)base.Owner; public ExpandCollapseState ExpandCollapseState => ToState(Owner.IsDropDownOpen); + public bool ShowsMenu => true; public void Collapse() => Owner.IsDropDownOpen = false; public void Expand() => Owner.IsDropDownOpen = true; diff --git a/src/Avalonia.Controls/Automation/Provider/IExpandCollapseProvider.cs b/src/Avalonia.Controls/Automation/Provider/IExpandCollapseProvider.cs index f0308c226b4..a4691180a3f 100644 --- a/src/Avalonia.Controls/Automation/Provider/IExpandCollapseProvider.cs +++ b/src/Avalonia.Controls/Automation/Provider/IExpandCollapseProvider.cs @@ -11,6 +11,15 @@ public interface IExpandCollapseProvider /// ExpandCollapseState ExpandCollapseState { get; } + /// + /// Gets a value indicating whether expanding the element shows a menu of items to the user, + /// such as drop-down list. + /// + /// + /// Used in OSX to enable the "Show Menu" action on the element. + /// + bool ShowsMenu { get; } + /// /// Displays all child nodes, controls, or content of the control. /// diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index fb3759b77e4..81ed94bda90 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Avalonia.Automation; using Avalonia.Automation.Peers; using Avalonia.Automation.Provider; using Avalonia.Native.Interop; @@ -75,6 +76,19 @@ public IAvnAutomationPeer? RootPeer return Wrap(result); } + public int IsExpandCollapseProvider() => (_inner is IExpandCollapseProvider).AsComBool(); + + public int ExpandCollapseProvider_IsExpanded() => ((IExpandCollapseProvider)_inner).ExpandCollapseState switch + { + ExpandCollapseState.Expanded => 1, + ExpandCollapseState.PartiallyExpanded => 1, + _ => 0, + }; + + public int ExpandCollapseProvider_ShowsMenu() => ((IExpandCollapseProvider)_inner).ShowsMenu.AsComBool(); + public void ExpandCollapseProvider_Expand() => ((IExpandCollapseProvider)_inner).Expand(); + public void ExpandCollapseProvider_Collapse() => ((IExpandCollapseProvider)_inner).Collapse(); + public int IsInvokeProvider() => (_inner is IInvokeProvider).AsComBool(); public void InvokeProvider_Invoke() => ((IInvokeProvider)_inner).Invoke(); diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 9b74eef6e21..8585ee63f2b 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -816,6 +816,12 @@ interface IAvnAutomationPeer : IUnknown IAvnAutomationPeer* RootProvider_GetFocus(); IAvnAutomationPeer* RootProvider_GetPeerFromPoint(AvnPoint point); + bool IsExpandCollapseProvider(); + bool ExpandCollapseProvider_IsExpanded(); + bool ExpandCollapseProvider_ShowsMenu(); + void ExpandCollapseProvider_Expand(); + void ExpandCollapseProvider_Collapse(); + bool IsInvokeProvider(); void InvokeProvider_Invoke(); From 2df1e32aae1d2fbda00a8fef7374e460c5244efd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 30 Mar 2021 18:32:56 +0200 Subject: [PATCH 23/73] Expose max/min for slider controls etc. --- native/Avalonia.Native/src/OSX/automation.mm | 29 ++++++++++++++++++-- src/Avalonia.Native/AvnAutomationPeer.cs | 6 ++-- src/Avalonia.Native/avn.idl | 6 ++-- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index c3ca182b912..b1a2b9e94ed 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -157,6 +157,27 @@ - (id)accessibilityValue return [super accessibilityValue]; } +- (id)accessibilityMinValue +{ + if (_peer->IsRangeValueProvider()) + { + return [NSNumber numberWithDouble:_peer->RangeValueProvider_GetMinimum()]; + } + + return [super accessibilityMinValue]; +} + +- (id)accessibilityMaxValue +{ + if (_peer->IsRangeValueProvider()) + { + return [NSNumber numberWithDouble:_peer->RangeValueProvider_GetMaximum()]; + } + + return [super accessibilityMaxValue]; +} + + - (NSArray *)accessibilityChildren { if (_children == nullptr && _peer != nullptr) @@ -226,7 +247,7 @@ - (BOOL)isAccessibilityExpanded { if (!_peer->IsExpandCollapseProvider()) return NO; - return _peer->ExpandCollapseProvider_IsExpanded(); + return _peer->ExpandCollapseProvider_GetIsExpanded(); } - (void)setAccessibilityExpanded:(BOOL)accessibilityExpanded @@ -284,7 +305,7 @@ - (BOOL)isAccessibilitySelectorAllowed:(SEL)selector { if (selector == @selector(accessibilityPerformShowMenu)) { - return _peer->IsExpandCollapseProvider() && _peer->ExpandCollapseProvider_ShowsMenu(); + return _peer->IsExpandCollapseProvider() && _peer->ExpandCollapseProvider_GetShowsMenu(); } else if (selector == @selector(isAccessibilityExpanded)) { @@ -295,7 +316,9 @@ - (BOOL)isAccessibilitySelectorAllowed:(SEL)selector return _peer->IsInvokeProvider() || _peer->IsExpandCollapseProvider(); } else if (selector == @selector(accessibilityPerformIncrement) || - selector == @selector(accessibilityPerformDecrement)) + selector == @selector(accessibilityPerformDecrement) || + selector == @selector(accessibilityMinValue) || + selector == @selector(accessibilityMaxValue)) { return _peer->IsRangeValueProvider(); } diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index 81ed94bda90..dee7658d331 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -78,14 +78,14 @@ public IAvnAutomationPeer? RootPeer public int IsExpandCollapseProvider() => (_inner is IExpandCollapseProvider).AsComBool(); - public int ExpandCollapseProvider_IsExpanded() => ((IExpandCollapseProvider)_inner).ExpandCollapseState switch + public int ExpandCollapseProvider_GetIsExpanded() => ((IExpandCollapseProvider)_inner).ExpandCollapseState switch { ExpandCollapseState.Expanded => 1, ExpandCollapseState.PartiallyExpanded => 1, _ => 0, }; - public int ExpandCollapseProvider_ShowsMenu() => ((IExpandCollapseProvider)_inner).ShowsMenu.AsComBool(); + public int ExpandCollapseProvider_GetShowsMenu() => ((IExpandCollapseProvider)_inner).ShowsMenu.AsComBool(); public void ExpandCollapseProvider_Expand() => ((IExpandCollapseProvider)_inner).Expand(); public void ExpandCollapseProvider_Collapse() => ((IExpandCollapseProvider)_inner).Collapse(); @@ -94,6 +94,8 @@ public IAvnAutomationPeer? RootPeer public int IsRangeValueProvider() => (_inner is IRangeValueProvider).AsComBool(); public double RangeValueProvider_GetValue() => ((IRangeValueProvider)_inner).Value; + public double RangeValueProvider_GetMinimum() => ((IRangeValueProvider)_inner).Minimum; + public double RangeValueProvider_GetMaximum() => ((IRangeValueProvider)_inner).Maximum; public double RangeValueProvider_GetSmallChange() => ((IRangeValueProvider)_inner).SmallChange; public double RangeValueProvider_GetLargeChange() => ((IRangeValueProvider)_inner).LargeChange; public void RangeValueProvider_SetValue(double value) => ((IRangeValueProvider)_inner).SetValue(value); diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 8585ee63f2b..73494d3e43c 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -817,8 +817,8 @@ interface IAvnAutomationPeer : IUnknown IAvnAutomationPeer* RootProvider_GetPeerFromPoint(AvnPoint point); bool IsExpandCollapseProvider(); - bool ExpandCollapseProvider_IsExpanded(); - bool ExpandCollapseProvider_ShowsMenu(); + bool ExpandCollapseProvider_GetIsExpanded(); + bool ExpandCollapseProvider_GetShowsMenu(); void ExpandCollapseProvider_Expand(); void ExpandCollapseProvider_Collapse(); @@ -827,6 +827,8 @@ interface IAvnAutomationPeer : IUnknown bool IsRangeValueProvider(); double RangeValueProvider_GetValue(); + double RangeValueProvider_GetMinimum(); + double RangeValueProvider_GetMaximum(); double RangeValueProvider_GetSmallChange(); double RangeValueProvider_GetLargeChange(); void RangeValueProvider_SetValue(double value); From 7ca502ee10c0c99b47a34ae49c5f2e3e180ce761 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 28 Sep 2021 22:13:30 +0200 Subject: [PATCH 24/73] Update ApiCompatBaseline.txt --- src/Avalonia.Controls/ApiCompatBaseline.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index fac5923db50..6e895179e55 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -49,10 +49,13 @@ MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.Resized.s InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Func Avalonia.Platform.IWindowBaseImpl.AutomationStarted' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Func Avalonia.Platform.IWindowBaseImpl.AutomationStarted.get()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.AutomationStarted.set(System.Func)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean, System.Boolean)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract. -Total Issues: 56 +Total Issues: 59 From c34a825bf5507a2ea87c9afd9e6cea3ff53c7594 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 2 Oct 2021 11:39:00 +0200 Subject: [PATCH 25/73] Try to fix problem with ComboBox. Only the OS can create the root automation peer, so we need to take care to not accidentally try to create it from `PopupAutomationPeer`. --- .../Automation/Peers/PopupAutomationPeer.cs | 11 ++++++----- src/Avalonia.Controls/Control.cs | 6 ++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs index 4bad8fd1083..0f534e6aebb 100644 --- a/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Avalonia.Automation.Platform; using Avalonia.Controls; +using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.VisualTree; @@ -20,9 +21,9 @@ public PopupAutomationPeer(IAutomationNodeFactory factory, Popup owner) protected override IReadOnlyList? GetChildrenCore() { - var host = (IVisualTreeHost)Owner; - System.Diagnostics.Debug.WriteLine($"Popup children='{host}'"); - return host.Root is Control c ? new[] { GetOrCreatePeer(c) } : null; + var popupHost = ((IPopupHostProvider)Owner)?.PopupHost as Control; + var hostPeer = popupHost?.GetAutomationPeer(); + return hostPeer is object ? new[] { hostPeer } : null; } protected override bool IsContentElementCore() => false; @@ -45,8 +46,8 @@ private void PopupOpenedClosed(object sender, EventArgs e) private AutomationPeer? GetPopupRoot() { - var popupRoot = ((IVisualTreeHost)Owner).Root as Control; - return popupRoot is object ? GetOrCreatePeer(popupRoot) : null; + var popupRoot = ((IPopupHostProvider)Owner).PopupHost as Control; + return popupRoot?.GetAutomationPeer(); } } } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 60acaf82db0..f2e2cd1d8f1 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -235,6 +235,12 @@ protected virtual AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory f return new NoneAutomationPeer(factory, this); } + internal AutomationPeer? GetAutomationPeer() + { + VerifyAccess(); + return _automationPeer; + } + internal AutomationPeer GetOrCreateAutomationPeer(IAutomationNodeFactory factory) { VerifyAccess(); From acb49d66e992f9d1aa5d79d9d8c4dd0294c8edeb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 2 Oct 2021 14:37:19 +0200 Subject: [PATCH 26/73] Revert "Try to fix problem with ComboBox." This reverts commit c34a825bf5507a2ea87c9afd9e6cea3ff53c7594. --- .../Automation/Peers/PopupAutomationPeer.cs | 11 +++++------ src/Avalonia.Controls/Control.cs | 6 ------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs index 0f534e6aebb..4bad8fd1083 100644 --- a/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using Avalonia.Automation.Platform; using Avalonia.Controls; -using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.VisualTree; @@ -21,9 +20,9 @@ public PopupAutomationPeer(IAutomationNodeFactory factory, Popup owner) protected override IReadOnlyList? GetChildrenCore() { - var popupHost = ((IPopupHostProvider)Owner)?.PopupHost as Control; - var hostPeer = popupHost?.GetAutomationPeer(); - return hostPeer is object ? new[] { hostPeer } : null; + var host = (IVisualTreeHost)Owner; + System.Diagnostics.Debug.WriteLine($"Popup children='{host}'"); + return host.Root is Control c ? new[] { GetOrCreatePeer(c) } : null; } protected override bool IsContentElementCore() => false; @@ -46,8 +45,8 @@ private void PopupOpenedClosed(object sender, EventArgs e) private AutomationPeer? GetPopupRoot() { - var popupRoot = ((IPopupHostProvider)Owner).PopupHost as Control; - return popupRoot?.GetAutomationPeer(); + var popupRoot = ((IVisualTreeHost)Owner).Root as Control; + return popupRoot is object ? GetOrCreatePeer(popupRoot) : null; } } } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index f2e2cd1d8f1..60acaf82db0 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -235,12 +235,6 @@ protected virtual AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory f return new NoneAutomationPeer(factory, this); } - internal AutomationPeer? GetAutomationPeer() - { - VerifyAccess(); - return _automationPeer; - } - internal AutomationPeer GetOrCreateAutomationPeer(IAutomationNodeFactory factory) { VerifyAccess(); From 11c60b42944c08e3559808caf7c5afc38e6c5b14 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 2 Oct 2021 16:20:19 +0200 Subject: [PATCH 27/73] Decouple automation peers from platform nodes. Now peers are entirely unaware of the platform implementation. --- .../Resources/Resource.Designer.cs | 2 +- .../Resources/Resource.Designer.cs | 2 +- .../AutomationPropertyChangedEventArgs.cs | 23 + .../Automation/Peers/AutomationPeer.cs | 37 +- .../Automation/Peers/ButtonAutomationPeer.cs | 5 +- .../Peers/CheckBoxAutomationPeer.cs | 7 +- .../Peers/ComboBoxAutomationPeer.cs | 10 +- .../Peers/ComboBoxItemAutomationPeer.cs | 5 +- .../Peers/ContentControlAutomationPeer.cs | 7 +- .../Peers/ContextMenuAutomationPeer.cs | 7 +- .../Automation/Peers/ControlAutomationPeer.cs | 34 +- .../Automation/Peers/ImageAutomationPeer.cs | 7 +- .../Peers/ItemsControlAutomationPeer.cs | 9 +- .../Peers/ListItemAutomationPeer.cs | 7 +- .../Automation/Peers/MenuAutomationPeer.cs | 7 +- .../Peers/MenuItemAutomationPeer.cs | 7 +- .../Automation/Peers/NoneAutomationPeer.cs | 5 +- .../Automation/Peers/PopupAutomationPeer.cs | 9 +- .../Peers/PopupRootAutomationPeer.cs | 5 +- .../Peers/RangeBaseAutomationPeer.cs | 5 +- .../Peers/ScrollViewerAutomationPeer.cs | 5 +- .../SelectingItemsControlAutomationPeer.cs | 11 +- .../Automation/Peers/SliderAutomationPeer.cs | 5 +- .../Peers/TabControlAutomationPeer.cs | 7 +- .../Automation/Peers/TabItemAutomationPeer.cs | 7 +- .../Peers/TextBlockAutomationPeer.cs | 7 +- .../Automation/Peers/TextBoxAutomationPeer.cs | 7 +- .../Peers/ToggleButtonAutomationPeer.cs | 7 +- .../Peers/UnrealizedElementAutomationPeer.cs | 6 - .../Automation/Peers/WindowAutomationPeer.cs | 5 +- .../Peers/WindowBaseAutomationPeer.cs | 18 +- .../Automation/Platform/IAutomationNode.cs | 32 -- .../Platform/IAutomationNodeFactory.cs | 18 - .../Platform/IRootAutomationNode.cs | 20 - .../Automation/Provider/IRootProvider.cs | 4 +- src/Avalonia.Controls/Button.cs | 6 +- src/Avalonia.Controls/CheckBox.cs | 5 +- src/Avalonia.Controls/ComboBox.cs | 5 +- src/Avalonia.Controls/ComboBoxItem.cs | 5 +- src/Avalonia.Controls/ContextMenu.cs | 5 +- src/Avalonia.Controls/Control.cs | 16 +- src/Avalonia.Controls/Image.cs | 5 +- src/Avalonia.Controls/ItemsControl.cs | 5 +- src/Avalonia.Controls/ListBoxItem.cs | 5 +- src/Avalonia.Controls/Menu.cs | 5 +- src/Avalonia.Controls/MenuItem.cs | 5 +- .../Platform/IWindowBaseImpl.cs | 6 - .../Primitives/AccessText.cs | 5 +- src/Avalonia.Controls/Primitives/Popup.cs | 5 +- src/Avalonia.Controls/Primitives/PopupRoot.cs | 5 +- .../Primitives/ToggleButton.cs | 5 +- src/Avalonia.Controls/ScrollViewer.cs | 5 +- src/Avalonia.Controls/Slider.cs | 5 +- src/Avalonia.Controls/TabControl.cs | 5 +- src/Avalonia.Controls/TabItem.cs | 5 +- src/Avalonia.Controls/TextBlock.cs | 5 +- src/Avalonia.Controls/TextBox.cs | 5 +- src/Avalonia.Controls/TopLevel.cs | 2 - src/Avalonia.Controls/Window.cs | 5 +- src/Avalonia.Controls/WindowBase.cs | 20 - .../Remote/PreviewerWindowImpl.cs | 4 - src/Avalonia.DesignerSupport/Remote/Stubs.cs | 2 - src/Avalonia.Headless/HeadlessWindowImpl.cs | 2 - src/Avalonia.Native/AutomationNode.cs | 7 +- src/Avalonia.Native/AutomationNodeFactory.cs | 24 - src/Avalonia.Native/AvnAutomationPeer.cs | 3 +- src/Avalonia.Native/WindowImplBase.cs | 38 -- src/Avalonia.Native/avn.idl | 1 - src/Avalonia.X11/X11Window.cs | 2 - .../Automation/AutomationNode.Selection.cs | 4 +- .../Automation/AutomationNode.cs | 39 +- .../Automation/AutomationNodeFactory.cs | 19 - .../Automation/RootAutomationNode.cs | 18 +- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 13 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 2 - .../Automation/ControlAutomationPeerTests.cs | 506 +++++++++--------- 76 files changed, 466 insertions(+), 727 deletions(-) create mode 100644 src/Avalonia.Controls/Automation/AutomationPropertyChangedEventArgs.cs delete mode 100644 src/Avalonia.Controls/Automation/Platform/IAutomationNode.cs delete mode 100644 src/Avalonia.Controls/Automation/Platform/IAutomationNodeFactory.cs delete mode 100644 src/Avalonia.Controls/Automation/Platform/IRootAutomationNode.cs delete mode 100644 src/Avalonia.Native/AutomationNodeFactory.cs delete mode 100644 src/Windows/Avalonia.Win32/Automation/AutomationNodeFactory.cs diff --git a/samples/ControlCatalog.Android/Resources/Resource.Designer.cs b/samples/ControlCatalog.Android/Resources/Resource.Designer.cs index b1ca548e2c7..6ac66739868 100644 --- a/samples/ControlCatalog.Android/Resources/Resource.Designer.cs +++ b/samples/ControlCatalog.Android/Resources/Resource.Designer.cs @@ -14,7 +14,7 @@ namespace ControlCatalog.Android { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "12.0.99.19")] public partial class Resource { diff --git a/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs b/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs index 83db67fceef..4046e601616 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs +++ b/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs @@ -14,7 +14,7 @@ namespace Avalonia.AndroidTestApplication { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "12.0.99.19")] public partial class Resource { diff --git a/src/Avalonia.Controls/Automation/AutomationPropertyChangedEventArgs.cs b/src/Avalonia.Controls/Automation/AutomationPropertyChangedEventArgs.cs new file mode 100644 index 00000000000..b8018613f80 --- /dev/null +++ b/src/Avalonia.Controls/Automation/AutomationPropertyChangedEventArgs.cs @@ -0,0 +1,23 @@ +using System; + +#nullable enable + +namespace Avalonia.Automation +{ + public class AutomationPropertyChangedEventArgs : EventArgs + { + public AutomationPropertyChangedEventArgs( + AutomationProperty property, + object? oldValue, + object? newValue) + { + Property = property; + OldValue = oldValue; + NewValue = newValue; + } + + public AutomationProperty Property { get; } + public object? OldValue { get; } + public object? NewValue { get; } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index 85a0e665b2f..757fe9e158a 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; -using Avalonia.Automation.Platform; #nullable enable @@ -56,33 +54,6 @@ public enum AutomationControlType /// public abstract class AutomationPeer { - /// - /// Initializes a new instance of the class. - /// - /// - /// The factory to use to create the platform automation node. - /// - protected AutomationPeer(IAutomationNodeFactory factory) - { - Node = factory.CreateNode(this); - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// The platform automation node. - /// - protected AutomationPeer(IAutomationNode node) - { - Node = node; - } - - /// - /// Gets the related node in the platform UI Automation tree. - /// - public IAutomationNode Node { get; } - /// /// Attempts to bring the element associated with the automation peer into view. /// @@ -188,18 +159,20 @@ protected AutomationPeer(IAutomationNode node) /// true if a context menu is present for the element; otherwise false. public bool ShowContextMenu() => ShowContextMenuCore(); + public event EventHandler? PropertyChanged; + /// /// Raises an event to notify the automation client of a changed property value. /// - /// The property that changed. + /// The property that changed. /// The previous value of the property. /// The new value of the property. public void RaisePropertyChangedEvent( - AutomationProperty automationProperty, + AutomationProperty property, object? oldValue, object? newValue) { - Node.PropertyChanged(automationProperty, oldValue, newValue); + PropertyChanged?.Invoke(this, new AutomationPropertyChangedEventArgs(property, oldValue, newValue)); } protected virtual string GetLocalizedControlTypeCore() diff --git a/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs index 66e5bd7b491..f5d6dce0399 100644 --- a/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs @@ -1,4 +1,3 @@ -using Avalonia.Automation.Platform; using Avalonia.Automation.Provider; using Avalonia.Controls; @@ -9,8 +8,8 @@ namespace Avalonia.Automation.Peers public class ButtonAutomationPeer : ContentControlAutomationPeer, IInvokeProvider { - public ButtonAutomationPeer(IAutomationNodeFactory factory, Button owner) - : base(factory, owner) + public ButtonAutomationPeer(Button owner) + : base(owner) { } diff --git a/src/Avalonia.Controls/Automation/Peers/CheckBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/CheckBoxAutomationPeer.cs index 4e98cc7746b..7f4e4929355 100644 --- a/src/Avalonia.Controls/Automation/Peers/CheckBoxAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/CheckBoxAutomationPeer.cs @@ -1,5 +1,4 @@ -using Avalonia.Automation.Platform; -using Avalonia.Controls; +using Avalonia.Controls; #nullable enable @@ -7,8 +6,8 @@ namespace Avalonia.Automation.Peers { public class CheckBoxAutomationPeer : ToggleButtonAutomationPeer { - public CheckBoxAutomationPeer(IAutomationNodeFactory factory, CheckBox owner) - : base(factory, owner) + public CheckBoxAutomationPeer(CheckBox owner) + : base(owner) { } diff --git a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs index d8225dbc40f..c582c3d3724 100644 --- a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Avalonia.Automation.Platform; using Avalonia.Automation.Provider; using Avalonia.Controls; @@ -12,8 +11,8 @@ public class ComboBoxAutomationPeer : SelectingItemsControlAutomationPeer, { private UnrealizedSelectionPeer[]? _selection; - public ComboBoxAutomationPeer(IAutomationNodeFactory factory, ComboBox owner) - : base(factory, owner) + public ComboBoxAutomationPeer(ComboBox owner) + : base(owner) { } @@ -39,7 +38,7 @@ protected override AutomationControlType GetAutomationControlTypeCore() // peer to represent the unrealized item. if (Owner.SelectedItem is object selection) { - _selection ??= new[] { new UnrealizedSelectionPeer(Node.Factory, this) }; + _selection ??= new[] { new UnrealizedSelectionPeer(this) }; _selection[0].Item = selection; return _selection; } @@ -70,8 +69,7 @@ private class UnrealizedSelectionPeer : UnrealizedElementAutomationPeer private readonly ComboBoxAutomationPeer _owner; private object? _item; - public UnrealizedSelectionPeer(IAutomationNodeFactory factory, ComboBoxAutomationPeer owner) - : base(factory) + public UnrealizedSelectionPeer(ComboBoxAutomationPeer owner) { _owner = owner; } diff --git a/src/Avalonia.Controls/Automation/Peers/ComboBoxItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ComboBoxItemAutomationPeer.cs index 06141ae8351..70d29dbc873 100644 --- a/src/Avalonia.Controls/Automation/Peers/ComboBoxItemAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ComboBoxItemAutomationPeer.cs @@ -1,5 +1,4 @@ using System; -using Avalonia.Automation.Platform; using Avalonia.Automation.Provider; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -11,8 +10,8 @@ namespace Avalonia.Automation.Peers { public class ComboBoxItemAutomationPeer : ListItemAutomationPeer { - public ComboBoxItemAutomationPeer(IAutomationNodeFactory factory, ComboBoxItem owner) - : base(factory, owner) + public ComboBoxItemAutomationPeer(ComboBoxItem owner) + : base(owner) { } diff --git a/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs index db1c7e1aa7b..08e4f2a9265 100644 --- a/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs @@ -1,5 +1,4 @@ -using Avalonia.Automation.Platform; -using Avalonia.Controls; +using Avalonia.Controls; #nullable enable @@ -7,8 +6,8 @@ namespace Avalonia.Automation.Peers { public class ContentControlAutomationPeer : ControlAutomationPeer { - protected ContentControlAutomationPeer(IAutomationNodeFactory factory, ContentControl owner) - : base(factory, owner) + protected ContentControlAutomationPeer(ContentControl owner) + : base(owner) { } diff --git a/src/Avalonia.Controls/Automation/Peers/ContextMenuAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ContextMenuAutomationPeer.cs index 5631fcf5811..3230f33506e 100644 --- a/src/Avalonia.Controls/Automation/Peers/ContextMenuAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ContextMenuAutomationPeer.cs @@ -1,5 +1,4 @@ -using Avalonia.Automation.Platform; -using Avalonia.Controls; +using Avalonia.Controls; #nullable enable @@ -7,8 +6,8 @@ namespace Avalonia.Automation.Peers { public class ContextMenuAutomationPeer : ControlAutomationPeer { - public ContextMenuAutomationPeer(IAutomationNodeFactory factory, ContextMenu owner) - : base(factory, owner) + public ContextMenuAutomationPeer(ContextMenu owner) + : base(owner) { } diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index bd4e9dfcf41..947c88a1903 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Avalonia.Automation.Platform; using Avalonia.Controls; using Avalonia.VisualTree; @@ -19,15 +18,7 @@ public class ControlAutomationPeer : AutomationPeer private AutomationPeer? _parent; private bool _parentValid; - public ControlAutomationPeer(IAutomationNodeFactory factory, Control owner) - : base(factory) - { - Owner = owner ?? throw new ArgumentNullException("owner"); - Initialize(); - } - - protected ControlAutomationPeer(IAutomationNode node, Control owner) - : base(node) + public ControlAutomationPeer(Control owner) { Owner = owner ?? throw new ArgumentNullException("owner"); Initialize(); @@ -35,15 +26,18 @@ protected ControlAutomationPeer(IAutomationNode node, Control owner) public Control Owner { get; } - public static AutomationPeer GetOrCreatePeer(IAutomationNodeFactory factory, Control element) + public event EventHandler? ChildrenChanged; + + public AutomationPeer GetOrCreate(Control element) { - element = element ?? throw new ArgumentNullException("element"); - return element.GetOrCreateAutomationPeer(factory); + if (element == Owner) + return this; + return CreatePeerForElement(element); } - public AutomationPeer GetOrCreatePeer(Control element) + public static AutomationPeer CreatePeerForElement(Control element) { - return element == Owner ? this : GetOrCreatePeer(Node.Factory, element); + return element.GetOrCreateAutomationPeer(); } protected override void BringIntoViewCore() => Owner.BringIntoView(); @@ -79,7 +73,7 @@ protected override IReadOnlyList GetOrCreateChildrenCore() { if (child is Control c && c.IsVisible) { - result.Add(GetOrCreatePeer(c)); + result.Add(GetOrCreate(c)); } } @@ -89,7 +83,7 @@ protected override IReadOnlyList GetOrCreateChildrenCore() protected override AutomationPeer? GetLabeledByCore() { var label = AutomationProperties.GetLabeledBy(Owner); - return label is Control c ? GetOrCreatePeer(c) : null; + return label is Control c ? GetOrCreate(c) : null; } protected override string? GetNameCore() @@ -116,7 +110,7 @@ protected override IReadOnlyList GetOrCreateChildrenCore() protected void InvalidateChildren() { _childrenValid = false; - Node!.ChildrenChanged(); + ChildrenChanged?.Invoke(this, EventArgs.Empty); } /// @@ -185,7 +179,7 @@ private void OwnerPropertyChanged(object sender, AvaloniaPropertyChangedEventArg { var parent = Owner.GetVisualParent(); if (parent is Control c) - (GetOrCreatePeer(c) as ControlAutomationPeer)?.InvalidateChildren(); + (GetOrCreate(c) as ControlAutomationPeer)?.InvalidateChildren(); } else if (e.Property == Visual.TransformedBoundsProperty) { @@ -211,7 +205,7 @@ private void EnsureConnected() { if (parent is Control c) { - var parentPeer = GetOrCreatePeer(c); + var parentPeer = GetOrCreate(c); parentPeer.GetChildren(); } diff --git a/src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs index ad88941299e..45483414874 100644 --- a/src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs @@ -1,5 +1,4 @@ -using Avalonia.Automation.Platform; -using Avalonia.Controls; +using Avalonia.Controls; #nullable enable @@ -7,8 +6,8 @@ namespace Avalonia.Automation.Peers { public class ImageAutomationPeer : ControlAutomationPeer { - public ImageAutomationPeer(IAutomationNodeFactory factory, Control owner) - : base(factory, owner) + public ImageAutomationPeer(Control owner) + : base(owner) { } diff --git a/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs index 6eed22e6cff..0d35920e193 100644 --- a/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs @@ -1,5 +1,4 @@ -using Avalonia.Automation.Platform; -using Avalonia.Automation.Provider; +using Avalonia.Automation.Provider; using Avalonia.Controls; #nullable enable @@ -11,8 +10,8 @@ public class ItemsControlAutomationPeer : ControlAutomationPeer, IScrollProvider private bool _searchedForScrollable; private IScrollProvider? _scroller; - public ItemsControlAutomationPeer(IAutomationNodeFactory factory, ItemsControl owner) - : base(factory, owner) + public ItemsControlAutomationPeer(ItemsControl owner) + : base(owner) { } @@ -31,7 +30,7 @@ protected virtual IScrollProvider? Scroller if (!_searchedForScrollable) { if (Owner.GetValue(ListBox.ScrollProperty) is Control scrollable) - _scroller = GetOrCreatePeer(scrollable) as IScrollProvider; + _scroller = GetOrCreate(scrollable) as IScrollProvider; _searchedForScrollable = true; } diff --git a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs index 1b9a8354a1b..0621e81c1a1 100644 --- a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs @@ -1,5 +1,4 @@ using System; -using Avalonia.Automation.Platform; using Avalonia.Automation.Provider; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -12,8 +11,8 @@ namespace Avalonia.Automation.Peers public class ListItemAutomationPeer : ContentControlAutomationPeer, ISelectionItemProvider { - public ListItemAutomationPeer(IAutomationNodeFactory factory, ContentControl owner) - : base(factory, owner) + public ListItemAutomationPeer(ContentControl owner) + : base(owner) { } @@ -25,7 +24,7 @@ public ISelectionProvider? SelectionContainer { if (Owner.Parent is Control parent) { - var parentPeer = GetOrCreatePeer(parent); + var parentPeer = GetOrCreate(parent); return parentPeer as ISelectionProvider; } diff --git a/src/Avalonia.Controls/Automation/Peers/MenuAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/MenuAutomationPeer.cs index 38cd7a8d416..e223a0864f3 100644 --- a/src/Avalonia.Controls/Automation/Peers/MenuAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/MenuAutomationPeer.cs @@ -1,5 +1,4 @@ -using Avalonia.Automation.Platform; -using Avalonia.Controls; +using Avalonia.Controls; #nullable enable @@ -7,8 +6,8 @@ namespace Avalonia.Automation.Peers { public class MenuAutomationPeer : ControlAutomationPeer { - public MenuAutomationPeer(IAutomationNodeFactory factory, Menu owner) - : base(factory, owner) + public MenuAutomationPeer(Menu owner) + : base(owner) { } diff --git a/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs index 5ed1c5e5790..1994f004d6a 100644 --- a/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs @@ -1,5 +1,4 @@ -using Avalonia.Automation.Platform; -using Avalonia.Controls; +using Avalonia.Controls; using Avalonia.Controls.Primitives; #nullable enable @@ -8,8 +7,8 @@ namespace Avalonia.Automation.Peers { public class MenuItemAutomationPeer : ControlAutomationPeer { - public MenuItemAutomationPeer(IAutomationNodeFactory factory, MenuItem owner) - : base(factory, owner) + public MenuItemAutomationPeer(MenuItem owner) + : base(owner) { } diff --git a/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs index fa5e6c175e0..2963144ebf3 100644 --- a/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs @@ -1,4 +1,3 @@ -using Avalonia.Automation.Platform; using Avalonia.Controls; #nullable enable @@ -11,8 +10,8 @@ namespace Avalonia.Automation.Peers /// public class NoneAutomationPeer : ControlAutomationPeer { - public NoneAutomationPeer(IAutomationNodeFactory factory, Control owner) - : base(factory, owner) + public NoneAutomationPeer(Control owner) + : base(owner) { } diff --git a/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs index 4bad8fd1083..e6f827b9425 100644 --- a/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Avalonia.Automation.Platform; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.VisualTree; @@ -11,8 +10,8 @@ namespace Avalonia.Automation.Peers { public class PopupAutomationPeer : ControlAutomationPeer { - public PopupAutomationPeer(IAutomationNodeFactory factory, Popup owner) - : base(factory, owner) + public PopupAutomationPeer(Popup owner) + : base(owner) { owner.Opened += PopupOpenedClosed; owner.Closed += PopupOpenedClosed; @@ -22,7 +21,7 @@ public PopupAutomationPeer(IAutomationNodeFactory factory, Popup owner) { var host = (IVisualTreeHost)Owner; System.Diagnostics.Debug.WriteLine($"Popup children='{host}'"); - return host.Root is Control c ? new[] { GetOrCreatePeer(c) } : null; + return host.Root is Control c ? new[] { GetOrCreate(c) } : null; } protected override bool IsContentElementCore() => false; @@ -46,7 +45,7 @@ private void PopupOpenedClosed(object sender, EventArgs e) private AutomationPeer? GetPopupRoot() { var popupRoot = ((IVisualTreeHost)Owner).Root as Control; - return popupRoot is object ? GetOrCreatePeer(popupRoot) : null; + return popupRoot is object ? GetOrCreate(popupRoot) : null; } } } diff --git a/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs index f7e06e83eb5..fb717ef98be 100644 --- a/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs @@ -1,5 +1,4 @@ using System; -using Avalonia.Automation.Platform; using Avalonia.Controls.Primitives; #nullable enable @@ -8,8 +7,8 @@ namespace Avalonia.Automation.Peers { public class PopupRootAutomationPeer : WindowBaseAutomationPeer { - public PopupRootAutomationPeer(IAutomationNode node, PopupRoot owner) - : base(node, owner) + public PopupRootAutomationPeer(PopupRoot owner) + : base(owner) { if (owner.IsVisible) StartTrackingFocus(); diff --git a/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs index 31a2b7e7af7..1bb487b1614 100644 --- a/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs @@ -1,4 +1,3 @@ -using Avalonia.Automation.Platform; using Avalonia.Automation.Provider; using Avalonia.Controls.Primitives; @@ -8,8 +7,8 @@ namespace Avalonia.Automation.Peers { public abstract class RangeBaseAutomationPeer : ControlAutomationPeer, IRangeValueProvider { - public RangeBaseAutomationPeer(IAutomationNodeFactory factory, RangeBase owner) - : base(factory, owner) + public RangeBaseAutomationPeer(RangeBase owner) + : base(owner) { owner.PropertyChanged += OwnerPropertyChanged; } diff --git a/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs index 165df6c0322..c2474bb9b83 100644 --- a/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs @@ -1,5 +1,4 @@ using System; -using Avalonia.Automation.Platform; using Avalonia.Automation.Provider; using Avalonia.Controls; using Avalonia.Utilities; @@ -10,8 +9,8 @@ namespace Avalonia.Automation.Peers { public class ScrollViewerAutomationPeer : ControlAutomationPeer, IScrollProvider { - public ScrollViewerAutomationPeer(IAutomationNodeFactory factory, ScrollViewer owner) - : base(factory, owner) + public ScrollViewerAutomationPeer(ScrollViewer owner) + : base(owner) { } diff --git a/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs index b55f653a5d9..f372e3b7813 100644 --- a/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Avalonia.Automation.Platform; using Avalonia.Automation.Provider; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -16,16 +15,16 @@ public abstract class SelectingItemsControlAutomationPeer : ItemsControlAutomati { private ISelectionModel _selection; - protected SelectingItemsControlAutomationPeer(IAutomationNodeFactory factory, SelectingItemsControl owner) - : base(factory, owner) + protected SelectingItemsControlAutomationPeer(SelectingItemsControl owner) + : base(owner) { _selection = owner.GetValue(ListBox.SelectionProperty); _selection.SelectionChanged += OwnerSelectionChanged; owner.PropertyChanged += OwnerPropertyChanged; } - public bool CanSelectMultiple => GetSelectionModeCore().HasFlagCustom(SelectionMode.Multiple); - public bool IsSelectionRequired => GetSelectionModeCore().HasFlagCustom(SelectionMode.AlwaysSelected); + public bool CanSelectMultiple => GetSelectionModeCore().HasAllFlags(SelectionMode.Multiple); + public bool IsSelectionRequired => GetSelectionModeCore().HasAllFlags(SelectionMode.AlwaysSelected); public IReadOnlyList GetSelection() => GetSelectionCore() ?? Array.Empty(); protected virtual IReadOnlyList? GetSelectionCore() @@ -42,7 +41,7 @@ protected SelectingItemsControlAutomationPeer(IAutomationNodeFactory factory, Se if (container is Control c && ((IVisual)c).IsAttachedToVisualTree) { - var peer = GetOrCreatePeer(c); + var peer = GetOrCreate(c); if (peer is object) { diff --git a/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs index 172b9a1b544..907e7790463 100644 --- a/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs @@ -1,4 +1,3 @@ -using Avalonia.Automation.Platform; using Avalonia.Controls; #nullable enable @@ -7,8 +6,8 @@ namespace Avalonia.Automation.Peers { public class SliderAutomationPeer : RangeBaseAutomationPeer { - public SliderAutomationPeer(IAutomationNodeFactory factory, Slider owner) - : base(factory, owner) + public SliderAutomationPeer(Slider owner) + : base(owner) { } diff --git a/src/Avalonia.Controls/Automation/Peers/TabControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TabControlAutomationPeer.cs index d615be43f3b..e14e61a6e40 100644 --- a/src/Avalonia.Controls/Automation/Peers/TabControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/TabControlAutomationPeer.cs @@ -1,5 +1,4 @@ -using Avalonia.Automation.Platform; -using Avalonia.Controls; +using Avalonia.Controls; #nullable enable @@ -7,8 +6,8 @@ namespace Avalonia.Automation.Peers { public class TabControlAutomationPeer : SelectingItemsControlAutomationPeer { - public TabControlAutomationPeer(IAutomationNodeFactory factory, TabControl owner) - : base(factory, owner) + public TabControlAutomationPeer(TabControl owner) + : base(owner) { } diff --git a/src/Avalonia.Controls/Automation/Peers/TabItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TabItemAutomationPeer.cs index 20021b5b963..dc794da915e 100644 --- a/src/Avalonia.Controls/Automation/Peers/TabItemAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/TabItemAutomationPeer.cs @@ -1,12 +1,11 @@ -using Avalonia.Automation.Platform; -using Avalonia.Controls; +using Avalonia.Controls; namespace Avalonia.Automation.Peers { public class TabItemAutomationPeer : ListItemAutomationPeer { - public TabItemAutomationPeer(IAutomationNodeFactory factory, TabItem owner) - : base(factory, owner) + public TabItemAutomationPeer(TabItem owner) + : base(owner) { } diff --git a/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs index a2ddedffc63..37eaf6f7c0f 100644 --- a/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs @@ -1,5 +1,4 @@ -using Avalonia.Automation.Platform; -using Avalonia.Controls; +using Avalonia.Controls; #nullable enable @@ -7,8 +6,8 @@ namespace Avalonia.Automation.Peers { public class TextBlockAutomationPeer : ControlAutomationPeer { - public TextBlockAutomationPeer(IAutomationNodeFactory factory, TextBlock owner) - : base(factory, owner) + public TextBlockAutomationPeer(TextBlock owner) + : base(owner) { } diff --git a/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs index 99f50ddabbe..33b2ba58b6a 100644 --- a/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs @@ -1,5 +1,4 @@ -using Avalonia.Automation.Platform; -using Avalonia.Automation.Provider; +using Avalonia.Automation.Provider; using Avalonia.Controls; #nullable enable @@ -8,8 +7,8 @@ namespace Avalonia.Automation.Peers { public class TextBoxAutomationPeer : ControlAutomationPeer, IValueProvider { - public TextBoxAutomationPeer(IAutomationNodeFactory factory, TextBox owner) - : base(factory, owner) + public TextBoxAutomationPeer(TextBox owner) + : base(owner) { } diff --git a/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs index 4c410d8654d..dd0c506d29f 100644 --- a/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs @@ -1,5 +1,4 @@ -using Avalonia.Automation.Platform; -using Avalonia.Automation.Provider; +using Avalonia.Automation.Provider; using Avalonia.Controls.Primitives; #nullable enable @@ -8,8 +7,8 @@ namespace Avalonia.Automation.Peers { public class ToggleButtonAutomationPeer : ContentControlAutomationPeer, IToggleProvider { - public ToggleButtonAutomationPeer(IAutomationNodeFactory factory, ToggleButton owner) - : base(factory, owner) + public ToggleButtonAutomationPeer(ToggleButton owner) + : base(owner) { } diff --git a/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs index 5832b04dd7b..b388f21a17a 100644 --- a/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Avalonia.Automation.Platform; #nullable enable @@ -11,11 +10,6 @@ namespace Avalonia.Automation.Peers /// public abstract class UnrealizedElementAutomationPeer : AutomationPeer { - protected UnrealizedElementAutomationPeer(IAutomationNodeFactory factory) - : base(factory) - { - } - public void SetParent(AutomationPeer? parent) => TrySetParent(parent); protected override void BringIntoViewCore() => GetParent()?.BringIntoView(); protected override Rect GetBoundingRectangleCore() => GetParent()?.GetBoundingRectangle() ?? default; diff --git a/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs index 9629ae0294b..fbc6e9d4f4e 100644 --- a/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs @@ -1,5 +1,4 @@ using System; -using Avalonia.Automation.Platform; using Avalonia.Controls; #nullable enable @@ -8,8 +7,8 @@ namespace Avalonia.Automation.Peers { public class WindowAutomationPeer : WindowBaseAutomationPeer { - public WindowAutomationPeer(IAutomationNode node, Window owner) - : base(node, owner) + public WindowAutomationPeer(Window owner) + : base(owner) { if (owner.IsVisible) StartTrackingFocus(); diff --git a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs index f97d13c766b..b24685929ad 100644 --- a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs @@ -1,5 +1,5 @@ +using System; using System.ComponentModel; -using Avalonia.Automation.Platform; using Avalonia.Automation.Provider; using Avalonia.Controls; using Avalonia.Input; @@ -14,25 +14,27 @@ public class WindowBaseAutomationPeer : ControlAutomationPeer, IRootProvider { private Control? _focus; - public WindowBaseAutomationPeer(IAutomationNode node, WindowBase owner) - : base(node, owner) + public WindowBaseAutomationPeer(WindowBase owner) + : base(owner) { } public new WindowBase Owner => (WindowBase)base.Owner; public ITopLevelImpl PlatformImpl => Owner.PlatformImpl; + public event EventHandler? FocusChanged; + protected override AutomationControlType GetAutomationControlTypeCore() { return AutomationControlType.Window; } - public AutomationPeer? GetFocus() => _focus is object ? GetOrCreatePeer(_focus) : null; + public AutomationPeer? GetFocus() => _focus is object ? GetOrCreate(_focus) : null; public AutomationPeer? GetPeerFromPoint(Point p) { var hit = Owner.GetVisualAt(p)?.FindAncestorOfType(includeSelf: true); - return hit is object ? GetOrCreatePeer(hit) : null; + return hit is object ? GetOrCreate(hit) : null; } protected void StartTrackingFocus() @@ -54,8 +56,10 @@ private void OnFocusChanged(IInputElement? focus) if (_focus != oldFocus) { - var peer = _focus is object ? GetOrCreatePeer(_focus) : null; - ((IRootAutomationNode)Node).FocusChanged(peer); + var peer = _focus is object ? + _focus == Owner ? this : + GetOrCreate(_focus) : null; + FocusChanged?.Invoke(this, EventArgs.Empty); } } diff --git a/src/Avalonia.Controls/Automation/Platform/IAutomationNode.cs b/src/Avalonia.Controls/Automation/Platform/IAutomationNode.cs deleted file mode 100644 index cae3504a4a8..00000000000 --- a/src/Avalonia.Controls/Automation/Platform/IAutomationNode.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using Avalonia.Automation.Peers; - -#nullable enable - -namespace Avalonia.Automation.Platform -{ - /// - /// Represents a platform implementation of a node in the UI Automation tree. - /// - public interface IAutomationNode - { - /// - /// Gets a factory which can be used to create child nodes. - /// - IAutomationNodeFactory Factory { get; } - - /// - /// Called by the when the children of the peer change. - /// - void ChildrenChanged(); - - /// - /// Called by the when a property other than the parent, - /// children or root changes. - /// - /// The property that changed. - /// The previous value of the property. - /// The new value of the property. - void PropertyChanged(AutomationProperty property, object? oldValue, object? newValue); - } -} diff --git a/src/Avalonia.Controls/Automation/Platform/IAutomationNodeFactory.cs b/src/Avalonia.Controls/Automation/Platform/IAutomationNodeFactory.cs deleted file mode 100644 index da16611b9e5..00000000000 --- a/src/Avalonia.Controls/Automation/Platform/IAutomationNodeFactory.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Avalonia.Automation.Peers; - -#nullable enable - -namespace Avalonia.Automation.Platform -{ - /// - /// Creates nodes in the UI Automation tree of the underlying platform. - /// - public interface IAutomationNodeFactory - { - /// - /// Creates an automation node for a peer. - /// - /// The peer. - IAutomationNode CreateNode(AutomationPeer peer); - } -} diff --git a/src/Avalonia.Controls/Automation/Platform/IRootAutomationNode.cs b/src/Avalonia.Controls/Automation/Platform/IRootAutomationNode.cs deleted file mode 100644 index 8346a908c26..00000000000 --- a/src/Avalonia.Controls/Automation/Platform/IRootAutomationNode.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Avalonia.Automation.Peers; - -#nullable enable - -namespace Avalonia.Automation.Platform -{ - /// - /// Represents a platform implementation of a root node in the UI Automation tree. - /// - public interface IRootAutomationNode : IAutomationNode - { - /// - /// Called by the when its focus changes. - /// - /// - /// The automation peer for the newly focused control or null if no control is focused. - /// - void FocusChanged(AutomationPeer? focus); - } -} diff --git a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs index 698b4f26b9f..8ea53863eb8 100644 --- a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs +++ b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs @@ -1,4 +1,5 @@ -using Avalonia.Automation.Peers; +using System; +using Avalonia.Automation.Peers; using Avalonia.Platform; #nullable enable @@ -10,5 +11,6 @@ public interface IRootProvider ITopLevelImpl? PlatformImpl { get; } AutomationPeer? GetFocus(); AutomationPeer? GetPeerFromPoint(Point p); + event EventHandler? FocusChanged; } } diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 645e697d039..740f715fe8c 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Windows.Input; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Data; @@ -380,10 +379,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs } } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) - { - return new ButtonAutomationPeer(factory, this); - } + protected override AutomationPeer OnCreateAutomationPeer() => new ButtonAutomationPeer(this); protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { diff --git a/src/Avalonia.Controls/CheckBox.cs b/src/Avalonia.Controls/CheckBox.cs index 374ab33338c..f7b0dcfdc20 100644 --- a/src/Avalonia.Controls/CheckBox.cs +++ b/src/Avalonia.Controls/CheckBox.cs @@ -1,5 +1,4 @@ using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls.Primitives; namespace Avalonia.Controls @@ -9,9 +8,9 @@ namespace Avalonia.Controls /// public class CheckBox : ToggleButton { - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer() { - return new CheckBoxAutomationPeer(factory, this); + return new CheckBoxAutomationPeer(this); } } } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index a50132270be..70fb62dc699 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using System.Reactive.Disposables; using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; @@ -298,9 +297,9 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) _popup.Closed += PopupClosed; } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer() { - return new ComboBoxAutomationPeer(factory, this); + return new ComboBoxAutomationPeer(this); } internal void ItemFocused(ComboBoxItem dropDownItem) diff --git a/src/Avalonia.Controls/ComboBoxItem.cs b/src/Avalonia.Controls/ComboBoxItem.cs index 0d5846f2707..42ec6e43b91 100644 --- a/src/Avalonia.Controls/ComboBoxItem.cs +++ b/src/Avalonia.Controls/ComboBoxItem.cs @@ -1,7 +1,6 @@ using System; using System.Reactive.Linq; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; namespace Avalonia.Controls { @@ -16,9 +15,9 @@ public ComboBoxItem() .Subscribe(_ => (Parent as ComboBox)?.ItemFocused(this)); } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer() { - return new ComboBoxItemAutomationPeer(factory, this); + return new ComboBoxItemAutomationPeer(this); } } } diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index ade23308d07..0a4b518f575 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.ComponentModel; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using System.Linq; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Generators; @@ -320,9 +319,9 @@ protected override IItemContainerGenerator CreateItemContainerGenerator() return new MenuItemContainerGenerator(this); } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer() { - return new ContextMenuAutomationPeer(factory, this); + return new ContextMenuAutomationPeer(this); } private void Open(Control control, Control placementTarget, bool requestedByPointer) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 60acaf82db0..a6b16a5b279 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; @@ -230,12 +229,12 @@ protected override void OnLostFocus(RoutedEventArgs e) } } - protected virtual AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected virtual AutomationPeer OnCreateAutomationPeer() { - return new NoneAutomationPeer(factory, this); + return new NoneAutomationPeer(this); } - internal AutomationPeer GetOrCreateAutomationPeer(IAutomationNodeFactory factory) + internal AutomationPeer GetOrCreateAutomationPeer() { VerifyAccess(); @@ -244,17 +243,10 @@ internal AutomationPeer GetOrCreateAutomationPeer(IAutomationNodeFactory factory return _automationPeer; } - _automationPeer = OnCreateAutomationPeer(factory); + _automationPeer = OnCreateAutomationPeer(); return _automationPeer; } - internal void SetAutomationPeer(AutomationPeer peer) - { - if (_automationPeer is object) - throw new InvalidOperationException("Automation peer is already set."); - _automationPeer = peer ?? throw new ArgumentNullException(nameof(peer)); - } - protected override void OnPointerReleased(PointerReleasedEventArgs e) { base.OnPointerReleased(e); diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index ad5455ba063..aaf93cac26d 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -1,5 +1,4 @@ using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Metadata; @@ -127,9 +126,9 @@ protected override Size ArrangeOverride(Size finalSize) } } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer() { - return new ImageAutomationPeer(factory, this); + return new ImageAutomationPeer(this); } } } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index dbf57b712d6..e3bb7abaad3 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -5,7 +5,6 @@ using System.Linq; using Avalonia.Collections; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls.Generators; using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; @@ -326,9 +325,9 @@ protected override void OnKeyDown(KeyEventArgs e) base.OnKeyDown(e); } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer() { - return new ItemsControlAutomationPeer(factory, this); + return new ItemsControlAutomationPeer(this); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) diff --git a/src/Avalonia.Controls/ListBoxItem.cs b/src/Avalonia.Controls/ListBoxItem.cs index 5599a89b62e..66a46cab4a5 100644 --- a/src/Avalonia.Controls/ListBoxItem.cs +++ b/src/Avalonia.Controls/ListBoxItem.cs @@ -1,5 +1,4 @@ using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; @@ -36,9 +35,9 @@ public bool IsSelected set { SetValue(IsSelectedProperty, value); } } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer() { - return new ListItemAutomationPeer(factory, this); + return new ListItemAutomationPeer(this); } } } diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index d6a0dc6f125..ed70316a533 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -1,5 +1,4 @@ using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -94,9 +93,9 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) } } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer() { - return new MenuAutomationPeer(factory, this); + return new MenuAutomationPeer(this); } } } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index f773f8fe2ae..8213cf29dcb 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -4,7 +4,6 @@ using System.Reactive.Linq; using System.Windows.Input; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls.Generators; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; @@ -495,9 +494,9 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) } } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer() { - return new MenuItemAutomationPeer(factory, this); + return new MenuItemAutomationPeer(this); } protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs index 82b6f6666a3..5a9e99106b4 100644 --- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs @@ -1,6 +1,5 @@ using System; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; namespace Avalonia.Platform { @@ -48,11 +47,6 @@ public interface IWindowBaseImpl : ITopLevelImpl /// Action Activated { get; set; } - /// - /// Gets or sets a method called when automation is started on the window. - /// - Func AutomationStarted { get; set; } - /// /// Gets the platform window handle. /// diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 15c44e8c125..39f8cd938c3 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -1,6 +1,5 @@ using System; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Input; using Avalonia.Media; using Avalonia.Media.TextFormatting; @@ -108,9 +107,9 @@ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e } } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer() { - return new NoneAutomationPeer(factory, this); + return new NoneAutomationPeer(this); } internal static string RemoveAccessKeyMarker(string text) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index ae72ccfee12..0bfbbf948bd 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Reactive.Disposables; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls.Mixins; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Presenters; @@ -543,9 +542,9 @@ private void HandlePositionChange() } } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer() { - return new PopupAutomationPeer(factory, this); + return new PopupAutomationPeer(this); } private static IDisposable SubscribeToEventHandler(T target, TEventHandler handler, Action subscribe, Action unsubscribe) diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 0a0c0f30ad9..f7281c107a8 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Reactive.Disposables; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Interactivity; using Avalonia.Media; @@ -168,9 +167,9 @@ protected override sealed Size ArrangeSetBounds(Size size) return ClientSize; } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNode node) + protected override AutomationPeer OnCreateAutomationPeer() { - return new PopupRootAutomationPeer(node, this); + return new PopupRootAutomationPeer(this); } } } diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index 434d34928fd..6c213021326 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -1,6 +1,5 @@ using System; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Interactivity; @@ -171,9 +170,9 @@ protected virtual void OnIndeterminate(RoutedEventArgs e) RaiseEvent(e); } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer() { - return new ToggleButtonAutomationPeer(factory, this); + return new ToggleButtonAutomationPeer(this); } private void OnIsCheckedChanged(AvaloniaPropertyChangedEventArgs e) diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 7d9675706c5..eaa9ba43c3a 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -1,7 +1,6 @@ using System; using System.Reactive.Linq; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -712,9 +711,9 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) _scrollBarExpandSubscription = SubscribeToScrollBars(e); } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer() { - return new ScrollViewerAutomationPeer(factory, this); + return new ScrollViewerAutomationPeer(this); } private IDisposable SubscribeToScrollBars(TemplateAppliedEventArgs e) diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 698e1734bba..227c387ffbe 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -1,7 +1,6 @@ using System; using Avalonia.Collections; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; @@ -211,9 +210,9 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) _pointerMovedDispose = this.AddDisposableHandler(PointerMovedEvent, TrackMoved, RoutingStrategies.Tunnel); } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer() { - return new SliderAutomationPeer(factory, this); + return new SliderAutomationPeer(this); } protected override void OnKeyDown(KeyEventArgs e) diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 63c68b783c3..cc9dab986a3 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -2,7 +2,6 @@ using System.Linq; using Avalonia.Collections; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; @@ -233,9 +232,9 @@ protected override void OnPointerReleased(PointerReleasedEventArgs e) } } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer() { - return new TabControlAutomationPeer(factory, this); + return new TabControlAutomationPeer(this); } } } diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 90817c655de..846010ce771 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -1,5 +1,4 @@ using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; @@ -83,9 +82,9 @@ private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj) } } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer() { - return new TabItemAutomationPeer(factory, this); + return new TabItemAutomationPeer(this); } } } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 8b4af15bc86..27db48e3f4c 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -1,6 +1,5 @@ using System.Reactive.Linq; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; @@ -513,9 +512,9 @@ protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e InvalidateMeasure(); } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer() { - return new TextBlockAutomationPeer(factory, this); + return new TextBlockAutomationPeer(this); } private static bool IsValidMaxLines(int maxLines) => maxLines >= 0; diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index f28d792dc22..5b334d20294 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -15,7 +15,6 @@ using Avalonia.Utilities; using Avalonia.Controls.Metadata; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; namespace Avalonia.Controls { @@ -1055,9 +1054,9 @@ protected override void OnPointerReleased(PointerReleasedEventArgs e) } } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) + protected override AutomationPeer OnCreateAutomationPeer() { - return new TextBoxAutomationPeer(factory, this); + return new TextBoxAutomationPeer(this); } protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index a8e76a394b8..5d9a0c8eed5 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -1,7 +1,5 @@ using System; using System.Reactive.Linq; -using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Input; diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index bbb0c164508..50227445df8 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -5,7 +5,6 @@ using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Interactivity; @@ -1014,9 +1013,9 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs } } - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNode node) + protected override AutomationPeer OnCreateAutomationPeer() { - return new WindowAutomationPeer(node, this); + return new WindowAutomationPeer(this); } } } diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 905a6b552f2..a3fd584447c 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -4,7 +4,6 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Layout; @@ -65,7 +64,6 @@ public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver dependencyRe impl.Activated = HandleActivated; impl.Deactivated = HandleDeactivated; impl.PositionChanged = HandlePositionChanged; - impl.AutomationStarted = HandleAutomationStarted; } /// @@ -263,17 +261,6 @@ protected override void ArrangeCore(Rect finalRect) /// The actual size of the window. protected virtual Size ArrangeSetBounds(Size size) => size; - protected sealed override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) - { - throw new NotSupportedException( - "Automation peer for window controls must be created by the operating system."); - } - - protected virtual AutomationPeer OnCreateAutomationPeer(IAutomationNode node) - { - throw new NotImplementedException("OnCreateAutomationPeer must be implemented in a derived class."); - } - /// /// Handles a window position change notification from /// . @@ -311,13 +298,6 @@ private void HandleDeactivated() Deactivated?.Invoke(this, EventArgs.Empty); } - private AutomationPeer HandleAutomationStarted(IAutomationNode node) - { - var peer = OnCreateAutomationPeer(node); - SetAutomationPeer(peer); - return peer; - } - private void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) { if (!_ignoreVisibilityChange) diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index ec685191d13..12af602f544 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -1,7 +1,4 @@ using System; -using System.Reactive.Disposables; -using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls; using Avalonia.Controls.Remote.Server; using Avalonia.Input; @@ -47,7 +44,6 @@ public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) public IPlatformHandle Handle { get; } public WindowState WindowState { get; set; } public Action WindowStateChanged { get; set; } - public Func AutomationStarted { get; set; } public Size MaxAutoSizeHint { get; } = new Size(4096, 4096); protected override void OnMessage(IAvaloniaRemoteTransportConnection transport, object obj) diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index a8e0ddad52c..8fb2c456b2b 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -4,7 +4,6 @@ using System.Reactive.Disposables; using System.Threading.Tasks; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives.PopupPositioning; @@ -41,7 +40,6 @@ class WindowStub : IWindowImpl, IPopupImpl public Action PositionChanged { get; set; } public WindowState WindowState { get; set; } public Action WindowStateChanged { get; set; } - public Func AutomationStarted { get; set; } public Action TransparencyLevelChanged { get; set; } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index 55a14769333..3bf7db34f6c 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Diagnostics; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Controls.Primitives.PopupPositioning; @@ -145,7 +144,6 @@ public void SetTopmost(bool value) public IScreenImpl Screen { get; } = new HeadlessScreensStub(); public WindowState WindowState { get; set; } public Action WindowStateChanged { get; set; } - public Func AutomationStarted { get; set; } public void SetTitle(string title) { diff --git a/src/Avalonia.Native/AutomationNode.cs b/src/Avalonia.Native/AutomationNode.cs index f85813a6fbd..251b7156f13 100644 --- a/src/Avalonia.Native/AutomationNode.cs +++ b/src/Avalonia.Native/AutomationNode.cs @@ -1,22 +1,19 @@ using Avalonia.Automation; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Native.Interop; #nullable enable namespace Avalonia.Native { - internal class AutomationNode : IRootAutomationNode + internal class AutomationNode { - public AutomationNode(AutomationNodeFactory factory, IAvnAutomationNode native) + public AutomationNode(IAvnAutomationNode native) { Native = native; - Factory = factory; } public IAvnAutomationNode Native { get; } - public IAutomationNodeFactory Factory { get; } public void ChildrenChanged() => Native.ChildrenChanged(); diff --git a/src/Avalonia.Native/AutomationNodeFactory.cs b/src/Avalonia.Native/AutomationNodeFactory.cs deleted file mode 100644 index d40009a855a..00000000000 --- a/src/Avalonia.Native/AutomationNodeFactory.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; -using Avalonia.Native.Interop; - -namespace Avalonia.Native -{ - internal class AutomationNodeFactory : IAutomationNodeFactory - { - private static AutomationNodeFactory _instance; - private readonly IAvaloniaNativeFactory _native; - - public static AutomationNodeFactory GetInstance(IAvaloniaNativeFactory native) - { - return _instance ??= new AutomationNodeFactory(native); - } - - private AutomationNodeFactory(IAvaloniaNativeFactory native) => _native = native; - - public IAutomationNode CreateNode(AutomationPeer peer) - { - return new AutomationNode(this, _native.CreateAutomationNode(new AvnAutomationPeer(peer))); - } - } -} diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index dee7658d331..b50b0d51111 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Avalonia.Automation; @@ -15,7 +16,7 @@ internal class AvnAutomationPeer : CallbackBase, IAvnAutomationPeer public AvnAutomationPeer(AutomationPeer inner) => _inner = inner; - public IAvnAutomationNode Node => ((AutomationNode)_inner.Node).Native; + public IAvnAutomationNode Node => throw new NotImplementedException(); public IAvnString? AcceleratorKey => _inner.GetAcceleratorKey().ToAvnString(); public IAvnString? AccessKey => _inner.GetAccessKey().ToAvnString(); public AvnAutomationControlType AutomationControlType => (AvnAutomationControlType)_inner.GetAutomationControlType(); diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 529e90ab492..d055e4a1c40 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Runtime.InteropServices; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Controls.Platform.Surfaces; @@ -63,9 +62,7 @@ internal abstract class WindowBaseImpl : IWindowBaseImpl, private GlPlatformSurface _glSurface; private NativeControlHostImpl _nativeControlHost; private IGlContext _glContext; - private IAutomationNode _automationNode; private AvnAutomationPeer _automationPeer; - private Func _automationStarted; internal WindowBaseImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, AvaloniaNativePlatformOpenGlInterface glFeature) @@ -265,8 +262,6 @@ public AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, return (AvnDragDropEffects)args.Effects; } } - - public IAvnAutomationPeer AutomationStarted(IAvnAutomationNode node) => _parent.HandleAutomationStarted(node); } public void Activate() @@ -489,38 +484,5 @@ public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0, 0); public IPlatformHandle Handle { get; private set; } - - public Func AutomationStarted - { - get => _automationStarted; - set - { - _automationStarted = value; - - // We've already received an AutomationStarted event, but the Window/PopupRoot wasn't initialized. - // Now it is, so notify it and store the automation peer for the next time the OS invokes an a11y - // query. - if (value is object && _automationNode is object) - _automationPeer = new AvnAutomationPeer(_automationStarted.Invoke(_automationNode)); - } - } - - private AvnAutomationPeer HandleAutomationStarted(IAvnAutomationNode node) - { - if (_automationPeer is object) - return _automationPeer; - - var factory = AutomationNodeFactory.GetInstance(_factory); - _automationNode = new AutomationNode(factory, node); - - // If automation is started during platform window creation we don't yet have a Window/PopupRoot - // control to notify. In this case we'll notify them when AutomationStarted gets set. We can safely - // return null here because the peer isn't actually needed at this point and will be re-queried the next - // time it's needed. - if (AutomationStarted is null) - return null; - - return _automationPeer = new AvnAutomationPeer(AutomationStarted(_automationNode)); - } } } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index da1bd7decde..3aec9d493ca 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -569,7 +569,6 @@ interface IAvnWindowBaseEvents : IUnknown AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, AvnInputModifiers modifiers, AvnDragDropEffects effects, IAvnClipboard* clipboard, [intptr]void* dataObjectHandle); - IAvnAutomationPeer* AutomationStarted(IAvnAutomationNode* node); } [uuid(1ae178ee-1fcc-447f-b6dd-b7bb727f934c)] diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 6783eada1ff..938a7f9b5db 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -7,7 +7,6 @@ using System.Text; using System.Threading.Tasks; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives.PopupPositioning; @@ -1163,6 +1162,5 @@ public void SetWindowManagerAddShadowHint(bool enabled) public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0.8); public bool NeedsManagedDecorations => false; - public Func AutomationStarted { get; set; } } } diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs index 17634ae8050..c41ace4aa84 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs @@ -20,14 +20,14 @@ public UIA.IRawElementProviderSimple? SelectionContainer get { var peer = InvokeSync(x => x.SelectionContainer); - return (peer as AutomationPeer)?.Node as AutomationNode; + return GetOrCreate(peer as AutomationPeer); } } public UIA.IRawElementProviderSimple[] GetSelection() { var peers = InvokeSync>(x => x.GetSelection()); - return peers?.Select(x => (UIA.IRawElementProviderSimple)x.Node).ToArray() ?? + return peers?.Select(x => (UIA.IRawElementProviderSimple)GetOrCreate(x)!).ToArray() ?? Array.Empty(); } diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index 79ef5bc7193..ebaf096b783 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -4,10 +4,10 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Avalonia.Automation; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Threading; using Avalonia.Win32.Interop.Automation; using AAP = Avalonia.Automation.Provider; @@ -18,7 +18,6 @@ namespace Avalonia.Win32.Automation { [ComVisible(true)] internal partial class AutomationNode : MarshalByRefObject, - IAutomationNode, IRawElementProviderSimple, IRawElementProviderSimple2, IRawElementProviderFragment, @@ -46,6 +45,9 @@ internal partial class AutomationNode : MarshalByRefObject, { SelectionPatternIdentifiers.SelectionProperty, UiaPropertyId.SelectionSelection }, }; + private static ConditionalWeakTable s_nodes = + new ConditionalWeakTable(); + private readonly int[] _runtimeId; private int _raiseFocusChanged; private int _raisePropertyChanged; @@ -54,22 +56,16 @@ public AutomationNode(AutomationPeer peer) { _runtimeId = new int[] { 3, GetHashCode() }; Peer = peer; - } - - protected AutomationNode(Func peerGetter) - { - _runtimeId = new int[] { 3, GetHashCode() }; - Peer = peerGetter(this); + s_nodes.Add(peer, this); } public AutomationPeer Peer { get; protected set; } - public IAutomationNodeFactory Factory => AutomationNodeFactory.Instance; public Rect BoundingRectangle { get => InvokeSync(() => { - if (GetRoot()?.Node is RootAutomationNode root) + if (GetRoot() is RootAutomationNode root) return root.ToScreen(Peer.GetBoundingRectangle()); return default; }); @@ -77,7 +73,7 @@ public Rect BoundingRectangle public virtual IRawElementProviderFragmentRoot? FragmentRoot { - get => InvokeSync(() => GetRoot())?.Node as IRawElementProviderFragmentRoot; + get => InvokeSync(() => GetRoot()) as IRawElementProviderFragmentRoot; } public virtual IRawElementProviderSimple? HostRawElementProvider => null; @@ -147,7 +143,7 @@ public void PropertyChanged(AutomationProperty property, object? oldValue, objec public virtual IRawElementProviderFragment? Navigate(NavigateDirection direction) { - IAutomationNode? GetSibling(int direction) + AutomationNode? GetSibling(int direction) { var children = Peer.GetParent()?.GetChildren(); @@ -157,7 +153,7 @@ public void PropertyChanged(AutomationProperty property, object? oldValue, objec { var j = i + direction; if (j >= 0 && j < children.Count) - return children[j].Node; + return GetOrCreate(children[j]); } } @@ -173,11 +169,11 @@ public void PropertyChanged(AutomationProperty property, object? oldValue, objec { return direction switch { - NavigateDirection.Parent => Peer.GetParent()?.Node, + NavigateDirection.Parent => GetOrCreate(Peer.GetParent()), NavigateDirection.NextSibling => GetSibling(1), NavigateDirection.PreviousSibling => GetSibling(-1), - NavigateDirection.FirstChild => Peer.GetChildren().FirstOrDefault()?.Node, - NavigateDirection.LastChild => Peer.GetChildren().LastOrDefault()?.Node, + NavigateDirection.FirstChild => GetOrCreate(Peer.GetChildren().FirstOrDefault()), + NavigateDirection.LastChild => GetOrCreate(Peer.GetChildren().LastOrDefault()), _ => null, }; }) as IRawElementProviderFragment; @@ -185,6 +181,13 @@ public void PropertyChanged(AutomationProperty property, object? oldValue, objec public void SetFocus() => InvokeSync(() => Peer.SetFocus()); + public static AutomationNode? GetOrCreate(AutomationPeer? peer) + { + if (peer is null) + return null; + return s_nodes.GetValue(peer, x => new AutomationNode(x)); + } + IRawElementProviderSimple[]? IRawElementProviderFragment.GetEmbeddedFragmentRoots() => null; void IRawElementProviderSimple2.ShowContextMenu() => InvokeSync(() => Peer.ShowContextMenu()); void IInvokeProvider.Invoke() => InvokeSync((AAP.IInvokeProvider x) => x.Invoke()); @@ -275,7 +278,7 @@ protected void RaiseFocusChanged(AutomationNode? focused) } } - private AutomationPeer GetRoot() + private AutomationNode? GetRoot() { Dispatcher.UIThread.VerifyAccess(); @@ -288,7 +291,7 @@ private AutomationPeer GetRoot() parent = peer.GetParent(); } - return peer; + return peer is object ? GetOrCreate(peer) : null; } private static UiaControlTypeId ToUiaControlType(AutomationControlType role) diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNodeFactory.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNodeFactory.cs deleted file mode 100644 index a7ee0e192f8..00000000000 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNodeFactory.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; -using Avalonia.Threading; - -#nullable enable - -namespace Avalonia.Win32.Automation -{ - internal class AutomationNodeFactory : IAutomationNodeFactory - { - public static readonly AutomationNodeFactory Instance = new AutomationNodeFactory(); - - public IAutomationNode CreateNode(AutomationPeer peer) - { - Dispatcher.UIThread.VerifyAccess(); - return new AutomationNode(peer); - } - } -} diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs index dd2665a624c..6728aa42834 100644 --- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -1,7 +1,6 @@ using System; using System.Runtime.InteropServices; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Automation.Provider; using Avalonia.Win32.Interop.Automation; @@ -10,12 +9,12 @@ namespace Avalonia.Win32.Automation { internal class RootAutomationNode : AutomationNode, - IRawElementProviderFragmentRoot, - IRootAutomationNode + IRawElementProviderFragmentRoot { - public RootAutomationNode(Func peerGetter) - : base(peerGetter) + public RootAutomationNode(AutomationPeer peer) + : base(peer) { + ((IRootProvider)peer).FocusChanged += FocusChanged; } public override IRawElementProviderFragmentRoot? FragmentRoot => this; @@ -30,20 +29,19 @@ public RootAutomationNode(Func peerGetter) var p = WindowImpl.PointToClient(new PixelPoint((int)x, (int)y)); var peer = (WindowBaseAutomationPeer)Peer; var found = InvokeSync(() => peer.GetPeerFromPoint(p)); - var result = found?.Node as IRawElementProviderFragment; + var result = GetOrCreate(found) as IRawElementProviderFragment; return result; } public IRawElementProviderFragment? GetFocus() { var focus = InvokeSync(() => Peer.GetFocus()); - return (AutomationNode?)focus?.Node; + return GetOrCreate(focus); } - public void FocusChanged(AutomationPeer? focus) + public void FocusChanged(object sender, EventArgs e) { - var node = focus?.Node as AutomationNode; - RaiseFocusChanged(node); + RaiseFocusChanged(GetOrCreate(Peer.GetFocus())); } public Rect ToScreen(Rect rect) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 8803901bc5c..fcc0ac1e323 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +using Avalonia.Automation.Peers; using Avalonia.Controls; using Avalonia.Controls.Remote; using Avalonia.Input; @@ -483,16 +484,14 @@ protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam, case WindowsMessage.WM_GETOBJECT: if ((long)lParam == UiaRootObjectId) { - if (_automationNode is null && AutomationStarted is object) + if (_automationNode is null) { - _automationNode = new RootAutomationNode(AutomationStarted); + var peer = ControlAutomationPeer.CreatePeerForElement((Control)_owner); + _automationNode = new RootAutomationNode(peer); } - if (_automationNode is object) - { - var r = UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, wParam, lParam, _automationNode); - return r; - } + var r = UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, wParam, lParam, _automationNode); + return r; } break; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 24bf409f587..ffad00cf956 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -4,7 +4,6 @@ using System.Runtime.InteropServices; using Avalonia.Controls; using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; @@ -177,7 +176,6 @@ egl.Display is AngleWin32EglDisplay angleDisplay && public Action LostFocus { get; set; } public Action TransparencyLevelChanged { get; set; } - public Func AutomationStarted { get; set; } public Thickness BorderThickness { diff --git a/tests/Avalonia.Controls.UnitTests/Automation/ControlAutomationPeerTests.cs b/tests/Avalonia.Controls.UnitTests/Automation/ControlAutomationPeerTests.cs index 250cad4e253..3a4bbd9928d 100644 --- a/tests/Avalonia.Controls.UnitTests/Automation/ControlAutomationPeerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Automation/ControlAutomationPeerTests.cs @@ -1,253 +1,253 @@ -using System.Linq; -using Avalonia.Automation.Peers; -using Avalonia.Automation.Platform; -using Avalonia.Automation.Provider; -using Avalonia.Controls.Presenters; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; -using Avalonia.Platform; -using Avalonia.UnitTests; -using Avalonia.VisualTree; -using Moq; -using Xunit; - -namespace Avalonia.Controls.UnitTests.Automation -{ - public class ControlAutomationPeerTests - { - private static Mock _factory; - - public ControlAutomationPeerTests() - { - _factory = new Mock(); - _factory.Setup(x => x.CreateNode(It.IsAny())) - .Returns(() => Mock.Of(x => x.Factory == _factory)); - } - - public class Children - { - [Fact] - public void Creates_Children_For_Controls_In_Visual_Tree() - { - var panel = new Panel - { - Children = - { - new Border(), - new Border(), - }, - }; - - var factory = CreateFactory(); - var target = CreatePeer(factory, panel); - - Assert.Equal( - panel.GetVisualChildren(), - target.GetChildren().Cast().Select(x => x.Owner)); - } - - [Fact] - public void Creates_Children_when_Controls_Attached_To_Visual_Tree() - { - var contentControl = new ContentControl - { - Template = new FuncControlTemplate((o, ns) => - new ContentPresenter - { - Name = "PART_ContentPresenter", - [!ContentPresenter.ContentProperty] = o[!ContentControl.ContentProperty], - }), - Content = new Border(), - }; - - var factory = CreateFactory(); - var target = CreatePeer(factory, contentControl); - - Assert.Empty(target.GetChildren()); - - contentControl.Measure(Size.Infinity); - - Assert.Equal(1, target.GetChildren().Count); - } - - [Fact] - public void Updates_Children_When_VisualChildren_Added() - { - var panel = new Panel - { - Children = - { - new Border(), - new Border(), - }, - }; - - var factory = CreateFactory(); - var target = CreatePeer(factory, panel); - var children = target.GetChildren(); - - Assert.Equal(2, children.Count); - - panel.Children.Add(new Decorator()); - - children = target.GetChildren(); - Assert.Equal(3, children.Count); - } - - [Fact] - public void Updates_Children_When_VisualChildren_Removed() - { - var panel = new Panel - { - Children = - { - new Border(), - new Border(), - }, - }; - - var factory = CreateFactory(); - var target = CreatePeer(factory, panel); - var children = target.GetChildren(); - - Assert.Equal(2, children.Count); - - panel.Children.RemoveAt(1); - - children = target.GetChildren(); - Assert.Equal(1, children.Count); - } - - [Fact] - public void Updates_Children_When_Visibility_Changes() - { - var panel = new Panel - { - Children = - { - new Border(), - new Border(), - }, - }; - - var factory = CreateFactory(); - var target = CreatePeer(factory, panel); - var children = target.GetChildren(); - - Assert.Equal(2, children.Count); - - panel.Children[1].IsVisible = false; - children = target.GetChildren(); - Assert.Equal(1, children.Count); - - panel.Children[1].IsVisible = true; - children = target.GetChildren(); - Assert.Equal(2, children.Count); - } - } - - public class Parent - { - [Fact] - public void Connects_Peer_To_Tree_When_GetParent_Called() - { - var border = new Border(); - var tree = new Decorator - { - Child = new Decorator - { - Child = border, - } - }; - - var factory = CreateFactory(); - - // We're accessing Border directly without going via its ancestors. Because the tree - // is built lazily, ensure that calling GetParent causes the ancestor tree to be built. - var target = CreatePeer(factory, border); - - var parentPeer = Assert.IsAssignableFrom(target.GetParent()); - Assert.Same(border.GetVisualParent(), parentPeer.Owner); - } - - [Fact] - public void Parent_Updated_When_Moved_To_Separate_Visual_Tree() - { - var border = new Border(); - var root1 = new Decorator { Child = border }; - var root2 = new Decorator(); - var factory = CreateFactory(); - var target = CreatePeer(factory, border); - - var parentPeer = Assert.IsAssignableFrom(target.GetParent()); - Assert.Same(root1, parentPeer.Owner); - - root1.Child = null; - - Assert.Null(target.GetParent()); - - root2.Child = border; - - parentPeer = Assert.IsAssignableFrom(target.GetParent()); - Assert.Same(root2, parentPeer.Owner); - } - } - - private static IAutomationNodeFactory CreateFactory() - { - var factory = new Mock(); - factory.Setup(x => x.CreateNode(It.IsAny())) - .Returns(() => Mock.Of(x => x.Factory == factory.Object)); - return factory.Object; - } - - private static AutomationPeer CreatePeer(IAutomationNodeFactory factory, Control control) - { - return ControlAutomationPeer.GetOrCreatePeer(factory, control); - } - - private class TestControl : Control - { - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) - { - return new TestAutomationPeer(factory, this); - } - } - - private class AutomationTestRoot : TestRoot - { - protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) - { - return new TestRootAutomationPeer(factory, this); - } - } - - private class TestAutomationPeer : ControlAutomationPeer - { - public TestAutomationPeer(IAutomationNodeFactory factory, Control owner) - : base(factory, owner) - { - } - } - - private class TestRootAutomationPeer : ControlAutomationPeer, IRootProvider - { - public TestRootAutomationPeer(IAutomationNodeFactory factory, Control owner) - : base(factory, owner) - { - } - - public ITopLevelImpl PlatformImpl => throw new System.NotImplementedException(); - - public AutomationPeer GetFocus() - { - throw new System.NotImplementedException(); - } - - public AutomationPeer GetPeerFromPoint(Point p) - { - throw new System.NotImplementedException(); - } - } - } -} +////using System.Linq; +////using Avalonia.Automation.Peers; +////using Avalonia.Automation.Platform; +////using Avalonia.Automation.Provider; +////using Avalonia.Controls.Presenters; +////using Avalonia.Controls.Primitives; +////using Avalonia.Controls.Templates; +////using Avalonia.Platform; +////using Avalonia.UnitTests; +////using Avalonia.VisualTree; +////using Moq; +////using Xunit; + +////namespace Avalonia.Controls.UnitTests.Automation +////{ +//// public class ControlAutomationPeerTests +//// { +//// private static Mock _factory; + +//// public ControlAutomationPeerTests() +//// { +//// _factory = new Mock(); +//// _factory.Setup(x => x.CreateNode(It.IsAny())) +//// .Returns(() => Mock.Of(x => x.Factory == _factory)); +//// } + +//// public class Children +//// { +//// [Fact] +//// public void Creates_Children_For_Controls_In_Visual_Tree() +//// { +//// var panel = new Panel +//// { +//// Children = +//// { +//// new Border(), +//// new Border(), +//// }, +//// }; + +//// var factory = CreateFactory(); +//// var target = CreatePeer(factory, panel); + +//// Assert.Equal( +//// panel.GetVisualChildren(), +//// target.GetChildren().Cast().Select(x => x.Owner)); +//// } + +//// [Fact] +//// public void Creates_Children_when_Controls_Attached_To_Visual_Tree() +//// { +//// var contentControl = new ContentControl +//// { +//// Template = new FuncControlTemplate((o, ns) => +//// new ContentPresenter +//// { +//// Name = "PART_ContentPresenter", +//// [!ContentPresenter.ContentProperty] = o[!ContentControl.ContentProperty], +//// }), +//// Content = new Border(), +//// }; + +//// var factory = CreateFactory(); +//// var target = CreatePeer(factory, contentControl); + +//// Assert.Empty(target.GetChildren()); + +//// contentControl.Measure(Size.Infinity); + +//// Assert.Equal(1, target.GetChildren().Count); +//// } + +//// [Fact] +//// public void Updates_Children_When_VisualChildren_Added() +//// { +//// var panel = new Panel +//// { +//// Children = +//// { +//// new Border(), +//// new Border(), +//// }, +//// }; + +//// var factory = CreateFactory(); +//// var target = CreatePeer(factory, panel); +//// var children = target.GetChildren(); + +//// Assert.Equal(2, children.Count); + +//// panel.Children.Add(new Decorator()); + +//// children = target.GetChildren(); +//// Assert.Equal(3, children.Count); +//// } + +//// [Fact] +//// public void Updates_Children_When_VisualChildren_Removed() +//// { +//// var panel = new Panel +//// { +//// Children = +//// { +//// new Border(), +//// new Border(), +//// }, +//// }; + +//// var factory = CreateFactory(); +//// var target = CreatePeer(factory, panel); +//// var children = target.GetChildren(); + +//// Assert.Equal(2, children.Count); + +//// panel.Children.RemoveAt(1); + +//// children = target.GetChildren(); +//// Assert.Equal(1, children.Count); +//// } + +//// [Fact] +//// public void Updates_Children_When_Visibility_Changes() +//// { +//// var panel = new Panel +//// { +//// Children = +//// { +//// new Border(), +//// new Border(), +//// }, +//// }; + +//// var factory = CreateFactory(); +//// var target = CreatePeer(factory, panel); +//// var children = target.GetChildren(); + +//// Assert.Equal(2, children.Count); + +//// panel.Children[1].IsVisible = false; +//// children = target.GetChildren(); +//// Assert.Equal(1, children.Count); + +//// panel.Children[1].IsVisible = true; +//// children = target.GetChildren(); +//// Assert.Equal(2, children.Count); +//// } +//// } + +//// public class Parent +//// { +//// [Fact] +//// public void Connects_Peer_To_Tree_When_GetParent_Called() +//// { +//// var border = new Border(); +//// var tree = new Decorator +//// { +//// Child = new Decorator +//// { +//// Child = border, +//// } +//// }; + +//// var factory = CreateFactory(); + +//// // We're accessing Border directly without going via its ancestors. Because the tree +//// // is built lazily, ensure that calling GetParent causes the ancestor tree to be built. +//// var target = CreatePeer(factory, border); + +//// var parentPeer = Assert.IsAssignableFrom(target.GetParent()); +//// Assert.Same(border.GetVisualParent(), parentPeer.Owner); +//// } + +//// [Fact] +//// public void Parent_Updated_When_Moved_To_Separate_Visual_Tree() +//// { +//// var border = new Border(); +//// var root1 = new Decorator { Child = border }; +//// var root2 = new Decorator(); +//// var factory = CreateFactory(); +//// var target = CreatePeer(factory, border); + +//// var parentPeer = Assert.IsAssignableFrom(target.GetParent()); +//// Assert.Same(root1, parentPeer.Owner); + +//// root1.Child = null; + +//// Assert.Null(target.GetParent()); + +//// root2.Child = border; + +//// parentPeer = Assert.IsAssignableFrom(target.GetParent()); +//// Assert.Same(root2, parentPeer.Owner); +//// } +//// } + +//// private static IAutomationNodeFactory CreateFactory() +//// { +//// var factory = new Mock(); +//// factory.Setup(x => x.CreateNode(It.IsAny())) +//// .Returns(() => Mock.Of(x => x.Factory == factory.Object)); +//// return factory.Object; +//// } + +//// private static AutomationPeer CreatePeer(IAutomationNodeFactory factory, Control control) +//// { +//// return ControlAutomationPeer.GetOrCreatePeer(factory, control); +//// } + +//// private class TestControl : Control +//// { +//// protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) +//// { +//// return new TestAutomationPeer(factory, this); +//// } +//// } + +//// private class AutomationTestRoot : TestRoot +//// { +//// protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) +//// { +//// return new TestRootAutomationPeer(factory, this); +//// } +//// } + +//// private class TestAutomationPeer : ControlAutomationPeer +//// { +//// public TestAutomationPeer(IAutomationNodeFactory factory, Control owner) +//// : base(factory, owner) +//// { +//// } +//// } + +//// private class TestRootAutomationPeer : ControlAutomationPeer, IRootProvider +//// { +//// public TestRootAutomationPeer(IAutomationNodeFactory factory, Control owner) +//// : base(factory, owner) +//// { +//// } + +//// public ITopLevelImpl PlatformImpl => throw new System.NotImplementedException(); + +//// public AutomationPeer GetFocus() +//// { +//// throw new System.NotImplementedException(); +//// } + +//// public AutomationPeer GetPeerFromPoint(Point p) +//// { +//// throw new System.NotImplementedException(); +//// } +//// } +//// } +////} From 8ffbbfb7fb59278c590fe0227cae336239b0c426 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 2 Oct 2021 22:40:22 +0200 Subject: [PATCH 28/73] Create root automation nodes like others. --- .../Avalonia.Win32/Automation/AutomationNode.cs | 16 ++++++++-------- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 14 ++++---------- src/Windows/Avalonia.Win32/WindowImpl.cs | 1 - 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index ebaf096b783..1b37664e4b2 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -160,11 +160,6 @@ public void PropertyChanged(AutomationProperty property, object? oldValue, objec return null; } - if (Peer.GetType().Name == "PopupAutomationPeer") - { - System.Diagnostics.Debug.WriteLine("Popup automation node navigate " + direction); - } - return InvokeSync(() => { return direction switch @@ -183,11 +178,11 @@ public void PropertyChanged(AutomationProperty property, object? oldValue, objec public static AutomationNode? GetOrCreate(AutomationPeer? peer) { - if (peer is null) - return null; - return s_nodes.GetValue(peer, x => new AutomationNode(x)); + return peer is null ? null : s_nodes.GetValue(peer, Create); } + public static void Release(AutomationPeer peer) => s_nodes.Remove(peer); + IRawElementProviderSimple[]? IRawElementProviderFragment.GetEmbeddedFragmentRoots() => null; void IRawElementProviderSimple2.ShowContextMenu() => InvokeSync(() => Peer.ShowContextMenu()); void IInvokeProvider.Invoke() => InvokeSync((AAP.IInvokeProvider x) => x.Invoke()); @@ -294,6 +289,11 @@ protected void RaiseFocusChanged(AutomationNode? focused) return peer is object ? GetOrCreate(peer) : null; } + private static AutomationNode Create(AutomationPeer peer) + { + return peer is AAP.IRootProvider ? new RootAutomationNode(peer) : new AutomationNode(peer); + } + private static UiaControlTypeId ToUiaControlType(AutomationControlType role) { return role switch diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index fcc0ac1e323..67c4aabafec 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -79,8 +79,7 @@ protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam, case WindowsMessage.WM_DESTROY: { - if (_automationNode is object) - UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, IntPtr.Zero, IntPtr.Zero, null); + UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, IntPtr.Zero, IntPtr.Zero, null); //Window doesn't exist anymore _hwnd = IntPtr.Zero; @@ -484,14 +483,9 @@ protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam, case WindowsMessage.WM_GETOBJECT: if ((long)lParam == UiaRootObjectId) { - if (_automationNode is null) - { - var peer = ControlAutomationPeer.CreatePeerForElement((Control)_owner); - _automationNode = new RootAutomationNode(peer); - } - - var r = UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, wParam, lParam, _automationNode); - return r; + var peer = ControlAutomationPeer.CreatePeerForElement((Control)_owner); + var node = AutomationNode.GetOrCreate(peer); + return UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, wParam, lParam, node); } break; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index ffad00cf956..f8c6c29fa1e 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -87,7 +87,6 @@ public partial class WindowImpl : IWindowImpl, private POINT _maxTrackSize; private WindowImpl _parent; private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default; - private AutomationNode _automationNode; private bool _isCloseRequested; private bool _shown; private bool _hiddenWindowIsParent; From ae8905776710351c361851bb89a202c0498d4bd6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 2 Oct 2021 22:44:39 +0200 Subject: [PATCH 29/73] Use non-obsolete interface. And remove debug code. --- .../Automation/Peers/PopupAutomationPeer.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs index e6f827b9425..9c24f855f8b 100644 --- a/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using Avalonia.Controls; +using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; -using Avalonia.VisualTree; #nullable enable @@ -19,9 +19,8 @@ public PopupAutomationPeer(Popup owner) protected override IReadOnlyList? GetChildrenCore() { - var host = (IVisualTreeHost)Owner; - System.Diagnostics.Debug.WriteLine($"Popup children='{host}'"); - return host.Root is Control c ? new[] { GetOrCreate(c) } : null; + var host = (IPopupHostProvider)Owner; + return host.PopupHost is Control c ? new[] { GetOrCreate(c) } : null; } protected override bool IsContentElementCore() => false; @@ -44,7 +43,7 @@ private void PopupOpenedClosed(object sender, EventArgs e) private AutomationPeer? GetPopupRoot() { - var popupRoot = ((IVisualTreeHost)Owner).Root as Control; + var popupRoot = ((IPopupHostProvider)Owner).PopupHost as Control; return popupRoot is object ? GetOrCreate(popupRoot) : null; } } From 26419fc345a85daad143451b346fdbbf2517a326 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 3 Oct 2021 21:13:47 +0200 Subject: [PATCH 30/73] Added AutomationControlType.None. --- src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs | 1 + src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs | 2 +- src/Windows/Avalonia.Win32/Automation/AutomationNode.cs | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index 757fe9e158a..f1276c68da5 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -7,6 +7,7 @@ namespace Avalonia.Automation.Peers { public enum AutomationControlType { + None, Button, Calendar, CheckBox, diff --git a/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs index 2963144ebf3..a9958861796 100644 --- a/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs @@ -17,7 +17,7 @@ public NoneAutomationPeer(Control owner) protected override AutomationControlType GetAutomationControlTypeCore() { - return AutomationControlType.Group; + return AutomationControlType.None; } protected override bool IsContentElementCore() => false; diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index 1b37664e4b2..66306572570 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -298,6 +298,7 @@ private static UiaControlTypeId ToUiaControlType(AutomationControlType role) { return role switch { + AutomationControlType.None => UiaControlTypeId.Group, AutomationControlType.Button => UiaControlTypeId.Button, AutomationControlType.Calendar => UiaControlTypeId.Calendar, AutomationControlType.CheckBox => UiaControlTypeId.CheckBox, From a5c28de7976cb7a460592fbfce444b3092fc0c56 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 3 Oct 2021 21:17:59 +0200 Subject: [PATCH 31/73] Reinstate ControlAutomationPeerTests. --- .../Automation/ControlAutomationPeerTests.cs | 482 +++++++++--------- 1 file changed, 229 insertions(+), 253 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Automation/ControlAutomationPeerTests.cs b/tests/Avalonia.Controls.UnitTests/Automation/ControlAutomationPeerTests.cs index 3a4bbd9928d..b128e6d83a8 100644 --- a/tests/Avalonia.Controls.UnitTests/Automation/ControlAutomationPeerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Automation/ControlAutomationPeerTests.cs @@ -1,253 +1,229 @@ -////using System.Linq; -////using Avalonia.Automation.Peers; -////using Avalonia.Automation.Platform; -////using Avalonia.Automation.Provider; -////using Avalonia.Controls.Presenters; -////using Avalonia.Controls.Primitives; -////using Avalonia.Controls.Templates; -////using Avalonia.Platform; -////using Avalonia.UnitTests; -////using Avalonia.VisualTree; -////using Moq; -////using Xunit; - -////namespace Avalonia.Controls.UnitTests.Automation -////{ -//// public class ControlAutomationPeerTests -//// { -//// private static Mock _factory; - -//// public ControlAutomationPeerTests() -//// { -//// _factory = new Mock(); -//// _factory.Setup(x => x.CreateNode(It.IsAny())) -//// .Returns(() => Mock.Of(x => x.Factory == _factory)); -//// } - -//// public class Children -//// { -//// [Fact] -//// public void Creates_Children_For_Controls_In_Visual_Tree() -//// { -//// var panel = new Panel -//// { -//// Children = -//// { -//// new Border(), -//// new Border(), -//// }, -//// }; - -//// var factory = CreateFactory(); -//// var target = CreatePeer(factory, panel); - -//// Assert.Equal( -//// panel.GetVisualChildren(), -//// target.GetChildren().Cast().Select(x => x.Owner)); -//// } - -//// [Fact] -//// public void Creates_Children_when_Controls_Attached_To_Visual_Tree() -//// { -//// var contentControl = new ContentControl -//// { -//// Template = new FuncControlTemplate((o, ns) => -//// new ContentPresenter -//// { -//// Name = "PART_ContentPresenter", -//// [!ContentPresenter.ContentProperty] = o[!ContentControl.ContentProperty], -//// }), -//// Content = new Border(), -//// }; - -//// var factory = CreateFactory(); -//// var target = CreatePeer(factory, contentControl); - -//// Assert.Empty(target.GetChildren()); - -//// contentControl.Measure(Size.Infinity); - -//// Assert.Equal(1, target.GetChildren().Count); -//// } - -//// [Fact] -//// public void Updates_Children_When_VisualChildren_Added() -//// { -//// var panel = new Panel -//// { -//// Children = -//// { -//// new Border(), -//// new Border(), -//// }, -//// }; - -//// var factory = CreateFactory(); -//// var target = CreatePeer(factory, panel); -//// var children = target.GetChildren(); - -//// Assert.Equal(2, children.Count); - -//// panel.Children.Add(new Decorator()); - -//// children = target.GetChildren(); -//// Assert.Equal(3, children.Count); -//// } - -//// [Fact] -//// public void Updates_Children_When_VisualChildren_Removed() -//// { -//// var panel = new Panel -//// { -//// Children = -//// { -//// new Border(), -//// new Border(), -//// }, -//// }; - -//// var factory = CreateFactory(); -//// var target = CreatePeer(factory, panel); -//// var children = target.GetChildren(); - -//// Assert.Equal(2, children.Count); - -//// panel.Children.RemoveAt(1); - -//// children = target.GetChildren(); -//// Assert.Equal(1, children.Count); -//// } - -//// [Fact] -//// public void Updates_Children_When_Visibility_Changes() -//// { -//// var panel = new Panel -//// { -//// Children = -//// { -//// new Border(), -//// new Border(), -//// }, -//// }; - -//// var factory = CreateFactory(); -//// var target = CreatePeer(factory, panel); -//// var children = target.GetChildren(); - -//// Assert.Equal(2, children.Count); - -//// panel.Children[1].IsVisible = false; -//// children = target.GetChildren(); -//// Assert.Equal(1, children.Count); - -//// panel.Children[1].IsVisible = true; -//// children = target.GetChildren(); -//// Assert.Equal(2, children.Count); -//// } -//// } - -//// public class Parent -//// { -//// [Fact] -//// public void Connects_Peer_To_Tree_When_GetParent_Called() -//// { -//// var border = new Border(); -//// var tree = new Decorator -//// { -//// Child = new Decorator -//// { -//// Child = border, -//// } -//// }; - -//// var factory = CreateFactory(); - -//// // We're accessing Border directly without going via its ancestors. Because the tree -//// // is built lazily, ensure that calling GetParent causes the ancestor tree to be built. -//// var target = CreatePeer(factory, border); - -//// var parentPeer = Assert.IsAssignableFrom(target.GetParent()); -//// Assert.Same(border.GetVisualParent(), parentPeer.Owner); -//// } - -//// [Fact] -//// public void Parent_Updated_When_Moved_To_Separate_Visual_Tree() -//// { -//// var border = new Border(); -//// var root1 = new Decorator { Child = border }; -//// var root2 = new Decorator(); -//// var factory = CreateFactory(); -//// var target = CreatePeer(factory, border); - -//// var parentPeer = Assert.IsAssignableFrom(target.GetParent()); -//// Assert.Same(root1, parentPeer.Owner); - -//// root1.Child = null; - -//// Assert.Null(target.GetParent()); - -//// root2.Child = border; - -//// parentPeer = Assert.IsAssignableFrom(target.GetParent()); -//// Assert.Same(root2, parentPeer.Owner); -//// } -//// } - -//// private static IAutomationNodeFactory CreateFactory() -//// { -//// var factory = new Mock(); -//// factory.Setup(x => x.CreateNode(It.IsAny())) -//// .Returns(() => Mock.Of(x => x.Factory == factory.Object)); -//// return factory.Object; -//// } - -//// private static AutomationPeer CreatePeer(IAutomationNodeFactory factory, Control control) -//// { -//// return ControlAutomationPeer.GetOrCreatePeer(factory, control); -//// } - -//// private class TestControl : Control -//// { -//// protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) -//// { -//// return new TestAutomationPeer(factory, this); -//// } -//// } - -//// private class AutomationTestRoot : TestRoot -//// { -//// protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory) -//// { -//// return new TestRootAutomationPeer(factory, this); -//// } -//// } - -//// private class TestAutomationPeer : ControlAutomationPeer -//// { -//// public TestAutomationPeer(IAutomationNodeFactory factory, Control owner) -//// : base(factory, owner) -//// { -//// } -//// } - -//// private class TestRootAutomationPeer : ControlAutomationPeer, IRootProvider -//// { -//// public TestRootAutomationPeer(IAutomationNodeFactory factory, Control owner) -//// : base(factory, owner) -//// { -//// } - -//// public ITopLevelImpl PlatformImpl => throw new System.NotImplementedException(); - -//// public AutomationPeer GetFocus() -//// { -//// throw new System.NotImplementedException(); -//// } - -//// public AutomationPeer GetPeerFromPoint(Point p) -//// { -//// throw new System.NotImplementedException(); -//// } -//// } -//// } -////} +using System; +using System.Linq; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Xunit; + +#nullable enable + +namespace Avalonia.Controls.UnitTests.Automation +{ + public class ControlAutomationPeerTests + { + public class Children + { + [Fact] + public void Creates_Children_For_Controls_In_Visual_Tree() + { + var panel = new Panel + { + Children = + { + new Border(), + new Border(), + }, + }; + + var target = CreatePeer(panel); + + Assert.Equal( + panel.GetVisualChildren(), + target.GetChildren().Cast().Select(x => x.Owner)); + } + + [Fact] + public void Creates_Children_when_Controls_Attached_To_Visual_Tree() + { + var contentControl = new ContentControl + { + Template = new FuncControlTemplate((o, ns) => + new ContentPresenter + { + Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = o[!ContentControl.ContentProperty], + }), + Content = new Border(), + }; + + var target = CreatePeer(contentControl); + + Assert.Empty(target.GetChildren()); + + contentControl.Measure(Size.Infinity); + + Assert.Equal(1, target.GetChildren().Count); + } + + [Fact] + public void Updates_Children_When_VisualChildren_Added() + { + var panel = new Panel + { + Children = + { + new Border(), + new Border(), + }, + }; + + var target = CreatePeer(panel); + var children = target.GetChildren(); + + Assert.Equal(2, children.Count); + + panel.Children.Add(new Decorator()); + + children = target.GetChildren(); + Assert.Equal(3, children.Count); + } + + [Fact] + public void Updates_Children_When_VisualChildren_Removed() + { + var panel = new Panel + { + Children = + { + new Border(), + new Border(), + }, + }; + + var target = CreatePeer(panel); + var children = target.GetChildren(); + + Assert.Equal(2, children.Count); + + panel.Children.RemoveAt(1); + + children = target.GetChildren(); + Assert.Equal(1, children.Count); + } + + [Fact] + public void Updates_Children_When_Visibility_Changes() + { + var panel = new Panel + { + Children = + { + new Border(), + new Border(), + }, + }; + + var target = CreatePeer(panel); + var children = target.GetChildren(); + + Assert.Equal(2, children.Count); + + panel.Children[1].IsVisible = false; + children = target.GetChildren(); + Assert.Equal(1, children.Count); + + panel.Children[1].IsVisible = true; + children = target.GetChildren(); + Assert.Equal(2, children.Count); + } + } + + public class Parent + { + [Fact] + public void Connects_Peer_To_Tree_When_GetParent_Called() + { + var border = new Border(); + var tree = new Decorator + { + Child = new Decorator + { + Child = border, + } + }; + + // We're accessing Border directly without going via its ancestors. Because the tree + // is built lazily, ensure that calling GetParent causes the ancestor tree to be built. + var target = CreatePeer(border); + + var parentPeer = Assert.IsAssignableFrom(target.GetParent()); + Assert.Same(border.GetVisualParent(), parentPeer.Owner); + } + + [Fact] + public void Parent_Updated_When_Moved_To_Separate_Visual_Tree() + { + var border = new Border(); + var root1 = new Decorator { Child = border }; + var root2 = new Decorator(); + var target = CreatePeer(border); + + var parentPeer = Assert.IsAssignableFrom(target.GetParent()); + Assert.Same(root1, parentPeer.Owner); + + root1.Child = null; + + Assert.Null(target.GetParent()); + + root2.Child = border; + + parentPeer = Assert.IsAssignableFrom(target.GetParent()); + Assert.Same(root2, parentPeer.Owner); + } + } + + private static AutomationPeer CreatePeer(Control control) + { + return ControlAutomationPeer.CreatePeerForElement(control); + } + + private class TestControl : Control + { + protected override AutomationPeer OnCreateAutomationPeer() + { + return new TestAutomationPeer(this); + } + } + + private class AutomationTestRoot : TestRoot + { + protected override AutomationPeer OnCreateAutomationPeer() + { + return new TestRootAutomationPeer(this); + } + } + + private class TestAutomationPeer : ControlAutomationPeer + { + public TestAutomationPeer( Control owner) + : base(owner) + { + } + } + + private class TestRootAutomationPeer : ControlAutomationPeer, IRootProvider + { + public TestRootAutomationPeer(Control owner) + : base(owner) + { + } + + public ITopLevelImpl PlatformImpl => throw new System.NotImplementedException(); + public event EventHandler? FocusChanged; + + public AutomationPeer GetFocus() + { + throw new System.NotImplementedException(); + } + + public AutomationPeer GetPeerFromPoint(Point p) + { + throw new System.NotImplementedException(); + } + } + } +} From 54d9ba7639f6a52817b385bc6a94ce178ec73a11 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 Oct 2021 17:19:53 +0200 Subject: [PATCH 32/73] Allow peers to dynamically expose implemented interfaces. --- .../Automation/Peers/AutomationPeer.cs | 13 +++++++++ .../Automation/AutomationNode.cs | 28 +++++++++++-------- .../Automation/RootAutomationNode.cs | 6 ++-- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index f1276c68da5..c3276db0588 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -160,6 +160,13 @@ public abstract class AutomationPeer /// true if a context menu is present for the element; otherwise false. public bool ShowContextMenu() => ShowContextMenuCore(); + /// + /// Tries to get a provider of the specified type from the peer. + /// + /// The provider type. + /// The provider, or null if not implemented on this peer. + public T? GetProvider() => (T?)GetProviderCore(typeof(T)); + public event EventHandler? PropertyChanged; /// @@ -223,6 +230,12 @@ protected virtual string GetLocalizedControlTypeCore() protected abstract bool IsKeyboardFocusableCore(); protected abstract void SetFocusCore(); protected abstract bool ShowContextMenuCore(); + + protected virtual object? GetProviderCore(Type providerType) + { + return providerType.IsAssignableFrom(this.GetType()) ? this : null; + } + protected internal abstract bool TrySetParent(AutomationPeer? parent); protected void EnsureEnabled() diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index 66306572570..7a8aa7b0fe8 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -99,17 +99,19 @@ public void PropertyChanged(AutomationProperty property, object? oldValue, objec [return: MarshalAs(UnmanagedType.IUnknown)] public virtual object? GetPatternProvider(int patternId) { + AutomationNode? ThisIfPeerImplementsProvider() => Peer.GetProvider() is object ? this : null; + return (UiaPatternId)patternId switch { - UiaPatternId.ExpandCollapse => Peer is IExpandCollapseProvider ? this : null, - UiaPatternId.Invoke => Peer is AAP.IInvokeProvider ? this : null, - UiaPatternId.RangeValue => Peer is AAP.IRangeValueProvider ? this : null, - UiaPatternId.Scroll => Peer is AAP.IScrollProvider ? this : null, + UiaPatternId.ExpandCollapse => ThisIfPeerImplementsProvider(), + UiaPatternId.Invoke => ThisIfPeerImplementsProvider(), + UiaPatternId.RangeValue => ThisIfPeerImplementsProvider(), + UiaPatternId.Scroll => ThisIfPeerImplementsProvider(), UiaPatternId.ScrollItem => this, - UiaPatternId.Selection => Peer is AAP.ISelectionProvider ? this : null, - UiaPatternId.SelectionItem => Peer is AAP.ISelectionItemProvider ? this : null, - UiaPatternId.Toggle => Peer is AAP.IToggleProvider ? this : null, - UiaPatternId.Value => Peer is AAP.IValueProvider ? this : null, + UiaPatternId.Selection => ThisIfPeerImplementsProvider(), + UiaPatternId.SelectionItem => ThisIfPeerImplementsProvider(), + UiaPatternId.Toggle => ThisIfPeerImplementsProvider(), + UiaPatternId.Value => ThisIfPeerImplementsProvider(), _ => null, }; } @@ -232,7 +234,7 @@ protected T InvokeSync(Func func) protected void InvokeSync(Action action) { - if (Peer is TInterface i) + if (Peer.GetProvider() is TInterface i) { try { @@ -248,7 +250,7 @@ protected void InvokeSync(Action action) [return: MaybeNull] protected TResult InvokeSync(Func func) { - if (Peer is TInterface i) + if (Peer.GetProvider() is TInterface i) { try { @@ -280,7 +282,7 @@ protected void RaiseFocusChanged(AutomationNode? focused) var peer = Peer; var parent = peer.GetParent(); - while (!(peer is AAP.IRootProvider) && parent is object) + while (peer.GetProvider() is null && parent is object) { peer = parent; parent = peer.GetParent(); @@ -291,7 +293,9 @@ protected void RaiseFocusChanged(AutomationNode? focused) private static AutomationNode Create(AutomationPeer peer) { - return peer is AAP.IRootProvider ? new RootAutomationNode(peer) : new AutomationNode(peer); + return peer.GetProvider() is object ? + new RootAutomationNode(peer) : + new AutomationNode(peer); } private static UiaControlTypeId ToUiaControlType(AutomationControlType role) diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs index 6728aa42834..435e0671df6 100644 --- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -14,11 +14,13 @@ internal class RootAutomationNode : AutomationNode, public RootAutomationNode(AutomationPeer peer) : base(peer) { - ((IRootProvider)peer).FocusChanged += FocusChanged; + Peer = base.Peer.GetProvider() ?? throw new AvaloniaInternalException( + "Attempt to create RootAutomationNode from peer which does not implement IRootProvider."); + Peer.FocusChanged += FocusChanged; } public override IRawElementProviderFragmentRoot? FragmentRoot => this; - public new IRootProvider Peer => (IRootProvider)base.Peer; + public new IRootProvider Peer { get; } public WindowImpl? WindowImpl => Peer.PlatformImpl as WindowImpl; public IRawElementProviderFragment? ElementProviderFromPoint(double x, double y) From 49ef4b3f280e4fdab2396f13da7d0a3ff4693e42 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 Oct 2021 22:51:39 +0200 Subject: [PATCH 33/73] Added AccessibilityView and ControlType. - `AutomationProperties.AccessibilityView`: from UWP - control's element's visibility in automation tree - `ControlTypeOverride`: from UWP proposal - overrides the automation control type specified in peer --- .../Automation/AutomationProperties.cs | 91 +++++++++++++++++++ .../Automation/Peers/AutomationPeer.cs | 9 +- .../Automation/Peers/ControlAutomationPeer.cs | 11 ++- 3 files changed, 106 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Automation/AutomationProperties.cs b/src/Avalonia.Controls/Automation/AutomationProperties.cs index 8ff1210a764..c20af148b8f 100644 --- a/src/Avalonia.Controls/Automation/AutomationProperties.cs +++ b/src/Avalonia.Controls/Automation/AutomationProperties.cs @@ -1,8 +1,30 @@ using System; +using Avalonia.Automation.Peers; using Avalonia.Controls; namespace Avalonia.Automation { + /// + /// Declares how a control should included in different views of the automation tree. + /// + public enum AccessibilityView + { + /// + /// The control is included in the Raw view of the automation tree. + /// + Raw, + + /// + /// The control is included in the Control view of the automation tree. + /// + Control, + + /// + /// The control is included in the Content view of the automation tree. + /// + Content, + } + public static class AutomationProperties { internal const int AutomationPositionInSetDefault = -1; @@ -16,6 +38,15 @@ public static class AutomationProperties "AcceleratorKey", typeof(AutomationProperties)); + /// + /// Defines the AutomationProperties.AccessibilityView attached property. + /// + public static readonly AttachedProperty AccessibilityViewProperty = + AvaloniaProperty.RegisterAttached( + "AccessibilityView", + typeof(AutomationProperties), + defaultValue: AccessibilityView.Content); + /// /// Defines the AutomationProperties.AccessKey attached property /// @@ -32,6 +63,14 @@ public static class AutomationProperties "AutomationId", typeof(AutomationProperties)); + /// + /// Defines the AutomationProperties.ControlTypeOverride attached property. + /// + public static readonly AttachedProperty ControlTypeOverrideProperty = + AvaloniaProperty.RegisterAttached( + "ControlTypeOverride", + typeof(AutomationProperties)); + /// /// Defines the AutomationProperties.HelpText attached property. /// @@ -171,6 +210,32 @@ public static string GetAcceleratorKey(StyledElement element) return ((string)element.GetValue(AcceleratorKeyProperty)); } + /// + /// Helper for setting AccessibilityView property on a StyledElement. + /// + public static void SetAccessibilityView(StyledElement element, AccessibilityView value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(AccessibilityViewProperty, value); + } + + /// + /// Helper for reading AccessibilityView property from a StyledElement. + /// + public static AccessibilityView GetAccessibilityView(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return element.GetValue(AccessibilityViewProperty); + } + /// /// Helper for setting AccessKey property on a StyledElement. /// @@ -223,6 +288,32 @@ public static string GetAutomationId(StyledElement element) return element.GetValue(AutomationIdProperty); } + /// + /// Helper for setting ControlTypeOverride property on a StyledElement. + /// + public static void SetControlTypeOverride(StyledElement element, AutomationControlType? value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(ControlTypeOverrideProperty, value); + } + + /// + /// Helper for reading ControlTypeOverride property from a StyledElement. + /// + public static AutomationControlType? GetControlTypeOverride(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return element.GetValue(ControlTypeOverrideProperty); + } + /// /// Helper for setting HelpText property on a StyledElement. /// diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index c3276db0588..fbca22031a0 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -74,7 +74,7 @@ public abstract class AutomationPeer /// /// Gets the control type for the element that is associated with the UI Automation peer. /// - public AutomationControlType GetAutomationControlType() => GetAutomationControlTypeCore(); + public AutomationControlType GetAutomationControlType() => GetControlTypeOverrideCore(); /// /// Gets the automation ID of the element that is associated with the UI Automation peer. @@ -230,7 +230,12 @@ protected virtual string GetLocalizedControlTypeCore() protected abstract bool IsKeyboardFocusableCore(); protected abstract void SetFocusCore(); protected abstract bool ShowContextMenuCore(); - + + protected virtual AutomationControlType GetControlTypeOverrideCore() + { + return GetAutomationControlTypeCore(); + } + protected virtual object? GetProviderCore(Type providerType) { return providerType.IsAssignableFrom(this.GetType()) ? this : null; diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index 947c88a1903..3cbcdcf3488 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -148,17 +148,22 @@ protected internal override bool TrySetParent(AutomationPeer? parent) 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.TransformedBounds); - protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Custom; 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 IsContentElementCore() => AutomationProperties.GetAccessibilityView(Owner) >= AccessibilityView.Content; + protected override bool IsControlElementCore() => AutomationProperties.GetAccessibilityView(Owner) >= AccessibilityView.Control; protected override bool IsEnabledCore() => Owner.IsEnabled; protected override bool IsKeyboardFocusableCore() => Owner.Focusable; protected override void SetFocusCore() => Owner.Focus(); + protected override AutomationControlType GetControlTypeOverrideCore() + { + return AutomationProperties.GetControlTypeOverride(Owner) ?? GetAutomationControlTypeCore(); + } + private static Rect GetBounds(TransformedBounds? bounds) { return bounds?.Bounds.TransformToAABB(bounds!.Value.Transform) ?? default; From 0756c1b3be4c8ebdf4d60ed568de896af3624f8f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 Oct 2021 23:27:33 +0200 Subject: [PATCH 34/73] Use new properties to remove some peers. Simple peers which only change the accessibility view or control type can now use the new `AutomationProperties`. --- .../Peers/CheckBoxAutomationPeer.cs | 19 --------------- .../Peers/ComboBoxItemAutomationPeer.cs | 23 ------------------- .../Peers/ContextMenuAutomationPeer.cs | 21 ----------------- .../Automation/Peers/ImageAutomationPeer.cs | 19 --------------- .../Automation/Peers/MenuAutomationPeer.cs | 21 ----------------- .../Automation/Peers/SliderAutomationPeer.cs | 19 --------------- .../Peers/TabControlAutomationPeer.cs | 19 --------------- .../Automation/Peers/TabItemAutomationPeer.cs | 17 -------------- src/Avalonia.Controls/CheckBox.cs | 5 ++-- src/Avalonia.Controls/ComboBoxItem.cs | 5 ++-- src/Avalonia.Controls/ContextMenu.cs | 8 +++---- src/Avalonia.Controls/Image.cs | 7 ++---- src/Avalonia.Controls/Menu.cs | 8 +++---- src/Avalonia.Controls/Slider.cs | 7 ++---- src/Avalonia.Controls/TabControl.cs | 7 ++---- src/Avalonia.Controls/TabItem.cs | 7 ++---- 16 files changed, 20 insertions(+), 192 deletions(-) delete mode 100644 src/Avalonia.Controls/Automation/Peers/CheckBoxAutomationPeer.cs delete mode 100644 src/Avalonia.Controls/Automation/Peers/ComboBoxItemAutomationPeer.cs delete mode 100644 src/Avalonia.Controls/Automation/Peers/ContextMenuAutomationPeer.cs delete mode 100644 src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs delete mode 100644 src/Avalonia.Controls/Automation/Peers/MenuAutomationPeer.cs delete mode 100644 src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs delete mode 100644 src/Avalonia.Controls/Automation/Peers/TabControlAutomationPeer.cs delete mode 100644 src/Avalonia.Controls/Automation/Peers/TabItemAutomationPeer.cs diff --git a/src/Avalonia.Controls/Automation/Peers/CheckBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/CheckBoxAutomationPeer.cs deleted file mode 100644 index 7f4e4929355..00000000000 --- a/src/Avalonia.Controls/Automation/Peers/CheckBoxAutomationPeer.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Avalonia.Controls; - -#nullable enable - -namespace Avalonia.Automation.Peers -{ - public class CheckBoxAutomationPeer : ToggleButtonAutomationPeer - { - public CheckBoxAutomationPeer(CheckBox owner) - : base(owner) - { - } - - protected override AutomationControlType GetAutomationControlTypeCore() - { - return AutomationControlType.CheckBox; - } - } -} diff --git a/src/Avalonia.Controls/Automation/Peers/ComboBoxItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ComboBoxItemAutomationPeer.cs deleted file mode 100644 index 70d29dbc873..00000000000 --- a/src/Avalonia.Controls/Automation/Peers/ComboBoxItemAutomationPeer.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using Avalonia.Automation.Provider; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Selection; - -#nullable enable - -namespace Avalonia.Automation.Peers -{ - public class ComboBoxItemAutomationPeer : ListItemAutomationPeer - { - public ComboBoxItemAutomationPeer(ComboBoxItem owner) - : base(owner) - { - } - - protected override AutomationControlType GetAutomationControlTypeCore() - { - return AutomationControlType.ComboBoxItem; - } - } -} diff --git a/src/Avalonia.Controls/Automation/Peers/ContextMenuAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ContextMenuAutomationPeer.cs deleted file mode 100644 index 3230f33506e..00000000000 --- a/src/Avalonia.Controls/Automation/Peers/ContextMenuAutomationPeer.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Avalonia.Controls; - -#nullable enable - -namespace Avalonia.Automation.Peers -{ - public class ContextMenuAutomationPeer : ControlAutomationPeer - { - public ContextMenuAutomationPeer(ContextMenu owner) - : base(owner) - { - } - - protected override AutomationControlType GetAutomationControlTypeCore() - { - return AutomationControlType.Menu; - } - - protected override bool IsContentElementCore() => false; - } -} diff --git a/src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs deleted file mode 100644 index 45483414874..00000000000 --- a/src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Avalonia.Controls; - -#nullable enable - -namespace Avalonia.Automation.Peers -{ - public class ImageAutomationPeer : ControlAutomationPeer - { - public ImageAutomationPeer(Control owner) - : base(owner) - { - } - - protected override AutomationControlType GetAutomationControlTypeCore() - { - return AutomationControlType.Image; - } - } -} diff --git a/src/Avalonia.Controls/Automation/Peers/MenuAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/MenuAutomationPeer.cs deleted file mode 100644 index e223a0864f3..00000000000 --- a/src/Avalonia.Controls/Automation/Peers/MenuAutomationPeer.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Avalonia.Controls; - -#nullable enable - -namespace Avalonia.Automation.Peers -{ - public class MenuAutomationPeer : ControlAutomationPeer - { - public MenuAutomationPeer(Menu owner) - : base(owner) - { - } - - protected override AutomationControlType GetAutomationControlTypeCore() - { - return AutomationControlType.Menu; - } - - protected override bool IsContentElementCore() => false; - } -} diff --git a/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs deleted file mode 100644 index 907e7790463..00000000000 --- a/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Avalonia.Controls; - -#nullable enable - -namespace Avalonia.Automation.Peers -{ - public class SliderAutomationPeer : RangeBaseAutomationPeer - { - public SliderAutomationPeer(Slider owner) - : base(owner) - { - } - - protected override AutomationControlType GetAutomationControlTypeCore() - { - return AutomationControlType.Slider; - } - } -} diff --git a/src/Avalonia.Controls/Automation/Peers/TabControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TabControlAutomationPeer.cs deleted file mode 100644 index e14e61a6e40..00000000000 --- a/src/Avalonia.Controls/Automation/Peers/TabControlAutomationPeer.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Avalonia.Controls; - -#nullable enable - -namespace Avalonia.Automation.Peers -{ - public class TabControlAutomationPeer : SelectingItemsControlAutomationPeer - { - public TabControlAutomationPeer(TabControl owner) - : base(owner) - { - } - - protected override AutomationControlType GetAutomationControlTypeCore() - { - return AutomationControlType.Tab; - } - } -} diff --git a/src/Avalonia.Controls/Automation/Peers/TabItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TabItemAutomationPeer.cs deleted file mode 100644 index dc794da915e..00000000000 --- a/src/Avalonia.Controls/Automation/Peers/TabItemAutomationPeer.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Avalonia.Controls; - -namespace Avalonia.Automation.Peers -{ - public class TabItemAutomationPeer : ListItemAutomationPeer - { - public TabItemAutomationPeer(TabItem owner) - : base(owner) - { - } - - protected override AutomationControlType GetAutomationControlTypeCore() - { - return AutomationControlType.TabItem; - } - } -} diff --git a/src/Avalonia.Controls/CheckBox.cs b/src/Avalonia.Controls/CheckBox.cs index f7b0dcfdc20..238a21393f8 100644 --- a/src/Avalonia.Controls/CheckBox.cs +++ b/src/Avalonia.Controls/CheckBox.cs @@ -1,3 +1,4 @@ +using Avalonia.Automation; using Avalonia.Automation.Peers; using Avalonia.Controls.Primitives; @@ -8,9 +9,9 @@ namespace Avalonia.Controls /// public class CheckBox : ToggleButton { - protected override AutomationPeer OnCreateAutomationPeer() + static CheckBox() { - return new CheckBoxAutomationPeer(this); + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.CheckBox); } } } diff --git a/src/Avalonia.Controls/ComboBoxItem.cs b/src/Avalonia.Controls/ComboBoxItem.cs index 42ec6e43b91..83057d139ff 100644 --- a/src/Avalonia.Controls/ComboBoxItem.cs +++ b/src/Avalonia.Controls/ComboBoxItem.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Linq; +using Avalonia.Automation; using Avalonia.Automation.Peers; namespace Avalonia.Controls @@ -15,9 +16,9 @@ public ComboBoxItem() .Subscribe(_ => (Parent as ComboBox)?.ItemFocused(this)); } - protected override AutomationPeer OnCreateAutomationPeer() + static ComboBoxItem() { - return new ComboBoxItemAutomationPeer(this); + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.ComboBoxItem); } } } diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 0a4b518f575..90c61aaed9c 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -14,6 +14,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Styling; +using Avalonia.Automation; #nullable enable @@ -110,6 +111,8 @@ static ContextMenu() ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); PlacementModeProperty.OverrideDefaultValue(PlacementMode.Pointer); ContextMenuProperty.Changed.Subscribe(ContextMenuChanged); + AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue(AccessibilityView.Control); + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Menu); } /// @@ -319,11 +322,6 @@ protected override IItemContainerGenerator CreateItemContainerGenerator() return new MenuItemContainerGenerator(this); } - protected override AutomationPeer OnCreateAutomationPeer() - { - return new ContextMenuAutomationPeer(this); - } - private void Open(Control control, Control placementTarget, bool requestedByPointer) { if (IsOpen) diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index aaf93cac26d..3d678806380 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -1,3 +1,4 @@ +using Avalonia.Automation; using Avalonia.Automation.Peers; using Avalonia.Media; using Avalonia.Media.Imaging; @@ -34,6 +35,7 @@ static Image() { AffectsRender(SourceProperty, StretchProperty, StretchDirectionProperty); AffectsMeasure(SourceProperty, StretchProperty, StretchDirectionProperty); + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Image); } /// @@ -125,10 +127,5 @@ protected override Size ArrangeOverride(Size finalSize) return new Size(); } } - - protected override AutomationPeer OnCreateAutomationPeer() - { - return new ImageAutomationPeer(this); - } } } diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index ed70316a533..4e71c99b024 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -1,3 +1,4 @@ +using Avalonia.Automation; using Avalonia.Automation.Peers; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; @@ -38,6 +39,8 @@ public Menu(IMenuInteractionHandler interactionHandler) static Menu() { ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel); + AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue(AccessibilityView.Control); + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Menu); } /// @@ -92,10 +95,5 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) inputRoot.AccessKeyHandler.MainMenu = this; } } - - protected override AutomationPeer OnCreateAutomationPeer() - { - return new MenuAutomationPeer(this); - } } } diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 227c387ffbe..dc8e27c3e2d 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -9,6 +9,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Utilities; +using Avalonia.Automation; namespace Avalonia.Controls { @@ -106,6 +107,7 @@ static Slider() RoutingStrategies.Bubble); ValueProperty.OverrideMetadata(new DirectPropertyMetadata(enableDataValidation: true)); + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Slider); } /// @@ -210,11 +212,6 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) _pointerMovedDispose = this.AddDisposableHandler(PointerMovedEvent, TrackMoved, RoutingStrategies.Tunnel); } - protected override AutomationPeer OnCreateAutomationPeer() - { - return new SliderAutomationPeer(this); - } - protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index cc9dab986a3..da204ffce87 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -10,6 +10,7 @@ using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.VisualTree; +using Avalonia.Automation; namespace Avalonia.Controls { @@ -69,6 +70,7 @@ static TabControl() ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); AffectsMeasure(TabStripPlacementProperty); SelectedItemProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent()); + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Tab); } /// @@ -231,10 +233,5 @@ protected override void OnPointerReleased(PointerReleasedEventArgs e) } } } - - protected override AutomationPeer OnCreateAutomationPeer() - { - return new TabControlAutomationPeer(this); - } } } diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 846010ce771..7f3df0ed8a5 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -1,3 +1,4 @@ +using Avalonia.Automation; using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; @@ -32,6 +33,7 @@ static TabItem() PressedMixin.Attach(); FocusableProperty.OverrideDefaultValue(typeof(TabItem), true); DataContextProperty.Changed.AddClassHandler((x, e) => x.UpdateHeader(e)); + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.TabItem); } /// @@ -81,10 +83,5 @@ private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj) } } } - - protected override AutomationPeer OnCreateAutomationPeer() - { - return new TabItemAutomationPeer(this); - } } } From eebcacac9a0bda61722a17997e8e52ad2645ac77 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 Oct 2021 23:38:57 +0200 Subject: [PATCH 35/73] Query correct interface. --- src/Windows/Avalonia.Win32/Automation/AutomationNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index 7a8aa7b0fe8..f2ba8759454 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -108,7 +108,7 @@ public void PropertyChanged(AutomationProperty property, object? oldValue, objec UiaPatternId.RangeValue => ThisIfPeerImplementsProvider(), UiaPatternId.Scroll => ThisIfPeerImplementsProvider(), UiaPatternId.ScrollItem => this, - UiaPatternId.Selection => ThisIfPeerImplementsProvider(), + UiaPatternId.Selection => ThisIfPeerImplementsProvider(), UiaPatternId.SelectionItem => ThisIfPeerImplementsProvider(), UiaPatternId.Toggle => ThisIfPeerImplementsProvider(), UiaPatternId.Value => ThisIfPeerImplementsProvider(), From f4f478c910ca1bda40f9b8ef29b2038854896340 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 28 Oct 2021 14:50:50 +0200 Subject: [PATCH 36/73] Move ChildrenChanged to AutomationPeer. --- .../Automation/Peers/AutomationPeer.cs | 13 +++++++++++++ .../Automation/Peers/ControlAutomationPeer.cs | 4 +--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index fbca22031a0..54b2fcc7fac 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -167,8 +167,21 @@ public abstract class AutomationPeer /// The provider, or null if not implemented on this peer. public T? GetProvider() => (T?)GetProviderCore(typeof(T)); + /// + /// Occurs when the children of the automation peer have changed. + /// + public event EventHandler? ChildrenChanged; + + /// + /// Occurs when a property value of the automation peer has changed. + /// public event EventHandler? PropertyChanged; + /// + /// Raises an event to notify the automation client the the children of the peer have changed. + /// + protected void RaiseChildrenChangedEvent() => ChildrenChanged?.Invoke(this, EventArgs.Empty); + /// /// Raises an event to notify the automation client of a changed property value. /// diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index 3cbcdcf3488..f7a993e16b8 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -26,8 +26,6 @@ public ControlAutomationPeer(Control owner) public Control Owner { get; } - public event EventHandler? ChildrenChanged; - public AutomationPeer GetOrCreate(Control element) { if (element == Owner) @@ -110,7 +108,7 @@ protected override IReadOnlyList GetOrCreateChildrenCore() protected void InvalidateChildren() { _childrenValid = false; - ChildrenChanged?.Invoke(this, EventArgs.Empty); + RaiseChildrenChangedEvent(); } /// From bc128676c456d3040f5c34b7040ce9d635aec185 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 10 Nov 2021 23:48:54 +0100 Subject: [PATCH 37/73] Make OSX a11y work again. Pretty major refactor of that code. --- native/Avalonia.Native/src/OSX/AvnString.mm | 2 +- native/Avalonia.Native/src/OSX/automation.h | 10 +- native/Avalonia.Native/src/OSX/automation.mm | 234 ++++++++++++------- native/Avalonia.Native/src/OSX/common.h | 3 - native/Avalonia.Native/src/OSX/main.mm | 8 +- native/Avalonia.Native/src/OSX/window.mm | 130 +++-------- src/Avalonia.Native/AutomationNode.cs | 40 ---- src/Avalonia.Native/Avalonia.Native.csproj | 4 + src/Avalonia.Native/AvnAutomationPeer.cs | 47 +++- src/Avalonia.Native/WindowImplBase.cs | 11 +- src/Avalonia.Native/avn.idl | 9 +- 11 files changed, 247 insertions(+), 251 deletions(-) delete mode 100644 src/Avalonia.Native/AutomationNode.cs diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index c69cef5c879..e0266a127c6 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -160,7 +160,7 @@ virtual HRESULT Get(unsigned int index, IAvnString**ppv) override { char* p; - if (s->Pointer((void**)&p) == S_OK) + if (s->Pointer((void**)&p) == S_OK && p != nullptr) { return [NSString stringWithUTF8String:p]; } diff --git a/native/Avalonia.Native/src/OSX/automation.h b/native/Avalonia.Native/src/OSX/automation.h index 65e1153248d..4a12a965fd8 100644 --- a/native/Avalonia.Native/src/OSX/automation.h +++ b/native/Avalonia.Native/src/OSX/automation.h @@ -1,16 +1,12 @@ #import +#include "window.h" NS_ASSUME_NONNULL_BEGIN class IAvnAutomationPeer; -@interface AvnAutomationNode : NSAccessibilityElement -- (AvnAutomationNode *)initWithPeer:(IAvnAutomationPeer *)peer; +@interface AvnAccessibilityElement : NSAccessibilityElement ++ (AvnAccessibilityElement *) acquire:(IAvnAutomationPeer *) peer; @end -struct INSAccessibilityHolder -{ - virtual NSObject* _Nonnull GetNSAccessibility () = 0; -}; - NS_ASSUME_NONNULL_END diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index b1a2b9e94ed..ca845d0ec8d 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -1,66 +1,108 @@ -#import "automation.h" #include "common.h" +#include "automation.h" #include "AvnString.h" #include "window.h" -class AutomationNode : public ComSingleObject, - public INSAccessibilityHolder +@interface AvnAccessibilityElement (Events) +- (void) raiseChildrenChanged; +@end + +@interface AvnRootAccessibilityElement : AvnAccessibilityElement +- (AvnView *) ownerView; +- (AvnRootAccessibilityElement *) initWithPeer:(IAvnAutomationPeer *) peer owner:(AvnView*) owner; +- (void) raiseFocusChanged; +@end + +class AutomationNode : public ComSingleObject { -private: - NSAccessibilityElement* _node; public: FORWARD_IUNKNOWN() - AutomationNode(NSAccessibilityElement* node) + AutomationNode(AvnAccessibilityElement* owner) { - _node = node; + _owner = owner; } - - AutomationNode(IAvnAutomationPeer* peer) + + AvnAccessibilityElement* GetOwner() { - _node = [[AvnAutomationNode alloc] initWithPeer: peer]; + return _owner; } - virtual void ChildrenChanged() override + virtual void Dispose() override { - NSAccessibilityPostNotification(_node, NSAccessibilityLayoutChangedNotification); } - virtual void PropertyChanged(AvnAutomationProperty property) override + virtual void ChildrenChanged () override { - switch (property) { - case RangeValueProvider_Value: - NSAccessibilityPostNotification(_node, NSAccessibilityValueChangedNotification); - break; - default: - break; - } + [_owner raiseChildrenChanged]; } - - virtual void FocusChanged(IAvnAutomationPeer* peer) override + + virtual void PropertyChanged (AvnAutomationProperty property) override { - // Only implemented in top-level nodes, i.e. AvnWindow. + } - - virtual NSObject* GetNSAccessibility() override + + virtual void FocusChanged () override { - return _node; + [(AvnRootAccessibilityElement*)_owner raiseFocusChanged]; } + +private: + AvnAccessibilityElement* _owner; }; -@implementation AvnAutomationNode +@implementation AvnAccessibilityElement { IAvnAutomationPeer* _peer; + AutomationNode* _node; NSMutableArray* _children; } -- (AvnAutomationNode *)initWithPeer:(IAvnAutomationPeer *)peer ++ (AvnAccessibilityElement *)acquire:(IAvnAutomationPeer *)peer +{ + if (peer == nullptr) + return nil; + + auto instance = peer->GetNode(); + + if (instance != nullptr) + return dynamic_cast(instance)->GetOwner(); + + if (peer->IsRootProvider()) + { + auto window = peer->RootProvider_GetWindow(); + auto holder = dynamic_cast(window); + auto view = holder->GetNSView(); + return [[AvnRootAccessibilityElement alloc] initWithPeer:peer owner:view]; + } + else + { + return [[AvnAccessibilityElement alloc] initWithPeer:peer]; + } +} + +- (AvnAccessibilityElement *)initWithPeer:(IAvnAutomationPeer *)peer { self = [super init]; _peer = peer; + _node = new AutomationNode(self); + _peer->SetNode(_node); return self; } +- (NSString *)description +{ + return [NSString stringWithFormat:@"%@ '%@' (%p)", + GetNSStringAndRelease(_peer->GetClassName()), + GetNSStringAndRelease(_peer->GetName()), + _peer]; +} + +- (IAvnAutomationPeer *)peer +{ + return _peer; +} + - (BOOL)isAccessibilityElement { return _peer->IsControlElement(); @@ -96,7 +138,7 @@ - (NSAccessibilityRole)accessibilityRole case AutomationToolBar: return NSAccessibilityToolbarRole; case AutomationToolTip: return NSAccessibilityPopoverRole; case AutomationTree: return NSAccessibilityOutlineRole; - case AutomationTreeItem: return NSAccessibilityOutlineRowSubrole; + case AutomationTreeItem: return NSAccessibilityCellRole; case AutomationCustom: return NSAccessibilityUnknownRole; case AutomationGroup: return NSAccessibilityGroupRole; case AutomationThumb: return NSAccessibilityHandleRole; @@ -110,8 +152,10 @@ - (NSAccessibilityRole)accessibilityRole case AutomationHeaderItem: return NSAccessibilityButtonRole; case AutomationTable: return NSAccessibilityTableRole; case AutomationTitleBar: return NSAccessibilityGroupRole; - case AutomationSeparator: return NSAccessibilityUnknownRole; - default: return NSAccessibilityUnknownRole; + // Treat unknown roles as generic group container items. Returning + // NSAccessibilityUnknownRole is also possible but makes the screen + // reader focus on the item instead of passing focus to child items. + default: return NSAccessibilityGroupRole; } } @@ -177,6 +221,15 @@ - (id)accessibilityMaxValue return [super accessibilityMaxValue]; } +- (BOOL)isAccessibilityEnabled +{ + return _peer->IsEnabled(); +} + +- (BOOL)isAccessibilityFocused +{ + return _peer->HasKeyboardFocus(); +} - (NSArray *)accessibilityChildren { @@ -195,52 +248,55 @@ - (NSArray *)accessibilityChildren if (childPeers->Get(i, &child) == S_OK) { - NSObject* element = ::GetAccessibilityElement(child->GetNode()); + auto element = [AvnAccessibilityElement acquire:child]; [_children addObject:element]; } } } } - + return _children; } - (NSRect)accessibilityFrame { - auto view = [self getAvnView]; - auto window = [self getAvnWindow]; + id topLevel = [self accessibilityTopLevelUIElement]; + auto result = NSZeroRect; - if (view != nullptr) + if ([topLevel isKindOfClass:[AvnRootAccessibilityElement class]]) { - auto bounds = ToNSRect(_peer->GetBoundingRectangle()); - auto windowBounds = [view convertRect:bounds toView:nil]; - auto screenBounds = [window convertRectToScreen:windowBounds]; - return screenBounds; + auto root = (AvnRootAccessibilityElement*)topLevel; + auto view = [root ownerView]; + + if (view) + { + auto window = [view window]; + auto bounds = ToNSRect(_peer->GetBoundingRectangle()); + auto windowBounds = [view convertRect:bounds toView:nil]; + auto screenBounds = [window convertRectToScreen:windowBounds]; + result = screenBounds; + } } - - return NSRect(); + + return result; } - (id)accessibilityParent { auto parentPeer = _peer->GetParent(); - - if (parentPeer != nullptr) - { - return GetAccessibilityElement(parentPeer); - } - - return [NSApplication sharedApplication]; + return parentPeer ? [AvnAccessibilityElement acquire:parentPeer] : [NSApplication sharedApplication]; } - (id)accessibilityTopLevelUIElement { - return GetAccessibilityElement([self getRootNode]); + auto rootPeer = _peer->GetRootPeer(); + return [AvnAccessibilityElement acquire:rootPeer]; } - (id)accessibilityWindow { - return [self accessibilityTopLevelUIElement]; + id topLevel = [self accessibilityTopLevelUIElement]; + return [topLevel isKindOfClass:[NSWindow class]] ? topLevel : nil; } - (BOOL)isAccessibilityExpanded @@ -326,58 +382,68 @@ - (BOOL)isAccessibilitySelectorAllowed:(SEL)selector return [super isAccessibilitySelectorAllowed:selector]; } -- (IAvnAutomationNode*)getRootNode +- (void)raiseChildrenChanged { - auto rootPeer = _peer->GetRootPeer(); - return rootPeer != nullptr ? rootPeer->GetNode() : nullptr; + NSAccessibilityPostNotification(self, NSAccessibilityLayoutChangedNotification); } -- (IAvnWindowBase*)getWindow +- (void)raisePropertyChanged { - auto rootNode = [self getRootNode]; +} - if (rootNode != nullptr) - { - IAvnWindowBase* window; - if (rootNode->QueryInterface(&IID_IAvnWindow, (void**)&window) == S_OK) - { - return window; - } - } - - return nullptr; +- (void)setAccessibilityFocused:(BOOL)accessibilityFocused +{ + if (accessibilityFocused) + _peer->SetFocus(); } -- (AvnWindow*) getAvnWindow +@end + +@implementation AvnRootAccessibilityElement { - auto window = [self getWindow]; - return window != nullptr ? dynamic_cast(window)->GetNSWindow() : nullptr; + AvnView* _owner; } -- (AvnView*) getAvnView +- (AvnRootAccessibilityElement *)initWithPeer:(IAvnAutomationPeer *)peer owner:(AvnView *)owner { - auto window = [self getWindow]; - return window != nullptr ? dynamic_cast(window)->GetNSView() : nullptr; + self = [super initWithPeer:peer]; + _owner = owner; + return self; } -@end +- (AvnView *)ownerView +{ + return _owner; +} -extern IAvnAutomationNode* CreateAutomationNode(IAvnAutomationPeer* peer) +- (id)accessibilityFocusedUIElement { - @autoreleasepool - { - return new AutomationNode(peer); - } + auto focusedPeer = [self peer]->RootProvider_GetFocus(); + return [AvnAccessibilityElement acquire:focusedPeer]; } -extern NSObject* GetAccessibilityElement(IAvnAutomationPeer* peer) +- (id)accessibilityHitTest:(NSPoint)point { - auto node = peer != nullptr ? peer->GetNode() : nullptr; - return GetAccessibilityElement(node); + auto clientPoint = [[_owner window] convertPointFromScreen:point]; + auto localPoint = [_owner translateLocalPoint:ToAvnPoint(clientPoint)]; + auto hit = [self peer]->RootProvider_GetPeerFromPoint(localPoint); + return [AvnAccessibilityElement acquire:hit]; } -extern NSObject* GetAccessibilityElement(IAvnAutomationNode* node) +- (id)accessibilityParent { - auto holder = dynamic_cast(node); - return holder != nullptr ? holder->GetNSAccessibility() : nil; + return _owner; } + +- (void)raiseFocusChanged +{ + id focused = [self accessibilityFocusedUIElement]; + NSAccessibilityPostNotification(focused, NSAccessibilityFocusedUIElementChangedNotification); +} + +- (void)accessibilityPerformAction:(NSAccessibilityActionName)action +{ + [_owner accessibilityPerformAction:action]; +} + +@end diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 53ff46cf325..091856fcf78 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -31,9 +31,6 @@ extern IAvnMenu* GetAppMenu (); extern NSMenuItem* GetAppMenuItem (); extern void SetAutoGenerateDefaultAppMenuItems (bool enabled); extern bool GetAutoGenerateDefaultAppMenuItems (); -extern IAvnAutomationNode* CreateAutomationNode(IAvnAutomationPeer* peer); -extern NSObject* GetAccessibilityElement(IAvnAutomationPeer* peer); -extern NSObject* GetAccessibilityElement(IAvnAutomationNode* node); extern void InitializeAvnApp(IAvnApplicationEvents* events); extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 68afb51f40c..9dc9da1cd1d 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -347,13 +347,7 @@ virtual HRESULT CreateMenuItemSeparator (IAvnMenuItem** ppv) override return S_OK; } } - - virtual HRESULT CreateAutomationNode (IAvnAutomationPeer* peer, IAvnAutomationNode** ppv) override - { - *ppv = ::CreateAutomationNode(peer); - return S_OK; - } - + virtual HRESULT SetAppMenu (IAvnMenu* appMenu) override { START_COM_CALL; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 47d605a3115..5b8a7a1c0e2 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -10,9 +10,7 @@ class WindowBaseImpl : public virtual ComObject, public virtual IAvnWindowBase, - public virtual IAvnAutomationNode, - public INSWindowHolder, - public INSAccessibilityHolder + public INSWindowHolder { private: NSCursor* cursor; @@ -21,7 +19,6 @@ FORWARD_IUNKNOWN() BEGIN_INTERFACE_MAP() INTERFACE_MAP_ENTRY(IAvnWindowBase, IID_IAvnWindowBase) - INTERFACE_MAP_ENTRY(IAvnAutomationNode, IID_IAvnAutomationNode) END_INTERFACE_MAP() virtual ~WindowBaseImpl() @@ -131,11 +128,6 @@ virtual HRESULT ObtainNSViewHandleRetained(void** ret) override return View; } - virtual NSObject* GetNSAccessibility() override - { - return Window; - } - virtual HRESULT Show(bool activate, bool isDialog) override { START_COM_CALL; @@ -603,25 +595,6 @@ virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint p return S_OK; } - virtual void ChildrenChanged() override - { - NSAccessibilityPostNotification(Window, NSAccessibilityLayoutChangedNotification); - } - - virtual void PropertyChanged(AvnAutomationProperty property) override - { - } - - virtual void FocusChanged(IAvnAutomationPeer* peer) override - { - auto element = GetAccessibilityElement(peer); - - if (element != nullptr) - { - NSAccessibilityPostNotification(element, NSAccessibilityFocusedUIElementChangedNotification); - } - } - virtual bool IsDialog() { return false; @@ -1432,6 +1405,7 @@ @implementation AvnView AvnPixelSize _lastPixelSize; NSObject* _renderTarget; AvnPlatformResizeReason _resizeReason; + AvnAccessibilityElement* _accessibilityChild; } - (void)onClosed @@ -2055,6 +2029,37 @@ - (void)setResizeReason:(AvnPlatformResizeReason)reason _resizeReason = reason; } +- (AvnAccessibilityElement *) accessibilityChild +{ + if (_accessibilityChild == nil) + { + auto peer = _parent->BaseEvents->GetAutomationPeer(); + + if (peer == nil) + return nil; + + _accessibilityChild = [AvnAccessibilityElement acquire:peer]; + } + + return _accessibilityChild; +} + +- (NSArray *)accessibilityChildren +{ + auto child = [self accessibilityChild]; + return NSAccessibilityUnignoredChildrenForOnlyChild(child); +} + +- (id)accessibilityHitTest:(NSPoint)point +{ + return [[self accessibilityChild] accessibilityHitTest:point]; +} + +- (id)accessibilityFocusedUIElement +{ + return [[self accessibilityChild] accessibilityFocusedUIElement]; +} + @end @@ -2466,75 +2471,6 @@ - (void)sendEvent:(NSEvent *)event } } -- (BOOL)isAccessibilityElement -{ - [self getAutomationPeer]; - return YES; -} - -- (NSString *)accessibilityIdentifier -{ - auto peer = [self getAutomationPeer]; - return GetNSStringAndRelease(peer->GetAutomationId()); -} - -- (NSArray *)accessibilityChildren -{ - auto peer = [self getAutomationPeer]; - - if (_automationChildren == nullptr) - { - _automationChildren = (NSMutableArray*)[super accessibilityChildren]; - - auto childPeers = peer->GetChildren(); - auto childCount = childPeers != nullptr ? childPeers->GetCount() : 0; - - if (childCount > 0) - { - for (int i = 0; i < childCount; ++i) - { - IAvnAutomationPeer* child; - - if (childPeers->Get(i, &child) == S_OK) - { - auto element = GetAccessibilityElement(child); - [_automationChildren addObject:element]; - } - } - } - } - - return _automationChildren; -} - -- (id)accessibilityHitTest:(NSPoint)point -{ - point = [self convertPointFromScreen:point]; - auto p = [_parent->View translateLocalPoint:ToAvnPoint(point)]; - auto peer = [self getAutomationPeer]; - auto hit = peer->RootProvider_GetPeerFromPoint(p); - return GetAccessibilityElement(hit); -} - -- (id)accessibilityFocusedUIElement -{ - auto peer = [self getAutomationPeer]; - - if (peer->IsRootProvider()) - { - return GetAccessibilityElement(peer->RootProvider_GetFocus()); - } - - return [super accessibilityFocusedUIElement]; -} - -- (IAvnAutomationPeer*) getAutomationPeer -{ - if (_automationPeer == nullptr) - _automationPeer = _parent->BaseEvents->AutomationStarted(_parent); - return _automationPeer; -} - @end class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup diff --git a/src/Avalonia.Native/AutomationNode.cs b/src/Avalonia.Native/AutomationNode.cs deleted file mode 100644 index 251b7156f13..00000000000 --- a/src/Avalonia.Native/AutomationNode.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Avalonia.Automation; -using Avalonia.Automation.Peers; -using Avalonia.Native.Interop; - -#nullable enable - -namespace Avalonia.Native -{ - internal class AutomationNode - { - public AutomationNode(IAvnAutomationNode native) - { - Native = native; - } - - public IAvnAutomationNode Native { get; } - - public void ChildrenChanged() => Native.ChildrenChanged(); - - public void PropertyChanged(AutomationProperty property, object? oldValue, object? newValue) - { - AvnAutomationProperty p; - - if (property == AutomationElementIdentifiers.BoundingRectangleProperty) - p = AvnAutomationProperty.AutomationPeer_BoundingRectangle; - else if (property == AutomationElementIdentifiers.ClassNameProperty) - p = AvnAutomationProperty.AutomationPeer_ClassName; - else if (property == AutomationElementIdentifiers.NameProperty) - p = AvnAutomationProperty.AutomationPeer_Name; - else if (property == RangeValuePatternIdentifiers.ValueProperty) - p = AvnAutomationProperty.RangeValueProvider_Value; - else - return; - - Native.PropertyChanged(p); - } - - public void FocusChanged(AutomationPeer? focus) => Native.FocusChanged(AvnAutomationPeer.Wrap(focus)); - } -} diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index 39134844315..b4304834191 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -16,6 +16,10 @@ PreserveNewest + + + + diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index b50b0d51111..bcb3d54fcfb 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.CompilerServices; using Avalonia.Automation; using Avalonia.Automation.Peers; using Avalonia.Automation.Provider; +using Avalonia.Controls; using Avalonia.Native.Interop; #nullable enable @@ -12,11 +15,20 @@ namespace Avalonia.Native { internal class AvnAutomationPeer : CallbackBase, IAvnAutomationPeer { + private static readonly ConditionalWeakTable s_wrappers = new(); private readonly AutomationPeer _inner; - public AvnAutomationPeer(AutomationPeer inner) => _inner = inner; + private AvnAutomationPeer(AutomationPeer inner) + { + _inner = inner; + _inner.ChildrenChanged += (_, _) => Node?.ChildrenChanged(); + if (inner is WindowBaseAutomationPeer window) + window.FocusChanged += (_, _) => Node?.FocusChanged(); + } - public IAvnAutomationNode Node => throw new NotImplementedException(); + ~AvnAutomationPeer() => Node?.Dispose(); + + public IAvnAutomationNode? Node { get; private set; } public IAvnString? AcceleratorKey => _inner.GetAcceleratorKey().ToAvnString(); public IAvnString? AccessKey => _inner.GetAccessKey().ToAvnString(); public AvnAutomationControlType AutomationControlType => (AvnAutomationControlType)_inner.GetAutomationControlType(); @@ -43,17 +55,31 @@ public IAvnAutomationPeer? RootPeer var peer = _inner; var parent = peer.GetParent(); - while (!(peer is IRootProvider) && parent is object) + while (peer is not IRootProvider && parent is not null) { peer = parent; parent = peer.GetParent(); } - return new AvnAutomationPeer(peer); + return Wrap(peer); } } + public void SetNode(IAvnAutomationNode node) + { + if (Node is not null) + throw new InvalidOperationException("The AvnAutomationPeer already has a node."); + Node = node; + } + public int IsRootProvider() => (_inner is IRootProvider).AsComBool(); + + public IAvnWindowBase RootProvider_GetWindow() + { + var window = (WindowBase)((ControlAutomationPeer)_inner).Owner; + return ((WindowBaseImpl)window.PlatformImpl!).Native; + } + public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(((IRootProvider)_inner).GetFocus()); public IAvnAutomationPeer? RootProvider_GetPeerFromPoint(AvnPoint point) @@ -68,7 +94,7 @@ public IAvnAutomationPeer? RootPeer { var parent = result.GetParent(); - if (parent is object) + if (parent is not null) result = parent; else break; @@ -108,9 +134,12 @@ public IAvnAutomationPeer? RootPeer public int IsValueProvider() => (_inner is IValueProvider).AsComBool(); public IAvnString ValueProvider_GetValue() => ((IValueProvider)_inner).Value.ToAvnString(); public void ValueProvider_SetValue(string value) => ((IValueProvider)_inner).SetValue(value); - - public static AvnAutomationPeer? Wrap(AutomationPeer? peer) => - peer != null ? new AvnAutomationPeer(peer) : null; + + [return: NotNullIfNotNull("peer")] + public static AvnAutomationPeer? Wrap(AutomationPeer? peer) + { + return peer is null ? null : s_wrappers.GetValue(peer, x => new(peer)); + } } internal class AvnAutomationPeerArray : CallbackBase, IAvnAutomationPeerArray @@ -119,7 +148,7 @@ internal class AvnAutomationPeerArray : CallbackBase, IAvnAutomationPeerArray public AvnAutomationPeerArray(IReadOnlyList items) { - _items = items.Select(x => new AvnAutomationPeer(x)).ToArray(); + _items = items.Select(x => AvnAutomationPeer.Wrap(x)).ToArray(); } public uint Count => (uint)_items.Length; diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index d055e4a1c40..fab72dfe478 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -62,7 +62,6 @@ internal abstract class WindowBaseImpl : IWindowBaseImpl, private GlPlatformSurface _glSurface; private NativeControlHostImpl _nativeControlHost; private IGlContext _glContext; - private AvnAutomationPeer _automationPeer; internal WindowBaseImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, AvaloniaNativePlatformOpenGlInterface glFeature) @@ -158,6 +157,11 @@ public ILockedFramebuffer Lock() public IMouseDevice MouseDevice => _mouse; public abstract IPopupImpl CreatePopup(); + public AutomationPeer GetAutomationPeer() + { + return _inputRoot is Control c ? ControlAutomationPeer.CreatePeerForElement(c) : null; + } + protected unsafe class WindowBaseEvents : CallbackBase, IAvnWindowBaseEvents { private readonly WindowBaseImpl _parent; @@ -262,6 +266,11 @@ public AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, return (AvnDragDropEffects)args.Effects; } } + + IAvnAutomationPeer IAvnWindowBaseEvents.AutomationPeer + { + get => AvnAutomationPeer.Wrap(_parent.GetAutomationPeer()); + } } public void Activate() diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index e16b5174b0a..954aeb3fb42 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -419,6 +419,7 @@ enum AvnPlatformResizeReason enum AvnAutomationControlType { + AutomationNone, AutomationButton, AutomationCalendar, AutomationCheckBox, @@ -480,7 +481,6 @@ interface IAvaloniaNativeFactory : IUnknown HRESULT CreateMenuItem(IAvnMenuItem** ppv); HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv); HRESULT CreateTrayIcon(IAvnTrayIcon** ppv); - HRESULT CreateAutomationNode(IAvnAutomationPeer* peer, IAvnAutomationNode** ppv); } [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)] @@ -570,6 +570,7 @@ interface IAvnWindowBaseEvents : IUnknown AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, AvnInputModifiers modifiers, AvnDragDropEffects effects, IAvnClipboard* clipboard, [intptr]void* dataObjectHandle); + IAvnAutomationPeer* GetAutomationPeer(); } [uuid(1ae178ee-1fcc-447f-b6dd-b7bb727f934c)] @@ -811,6 +812,8 @@ interface IAvnApplicationEvents : IUnknown interface IAvnAutomationPeer : IUnknown { IAvnAutomationNode* GetNode(); + void SetNode(IAvnAutomationNode* node); + IAvnString* GetAcceleratorKey(); IAvnString* GetAccessKey(); AvnAutomationControlType GetAutomationControlType(); @@ -832,6 +835,7 @@ interface IAvnAutomationPeer : IUnknown IAvnAutomationPeer* GetRootPeer(); bool IsRootProvider(); + IAvnWindowBase* RootProvider_GetWindow(); IAvnAutomationPeer* RootProvider_GetFocus(); IAvnAutomationPeer* RootProvider_GetPeerFromPoint(AvnPoint point); @@ -871,7 +875,8 @@ interface IAvnAutomationPeerArray : IUnknown [uuid(004dc40b-e435-49dc-bac5-6272ee35382a)] interface IAvnAutomationNode : IUnknown { + void Dispose(); void ChildrenChanged(); void PropertyChanged(AvnAutomationProperty property); - void FocusChanged(IAvnAutomationPeer* peer); + void FocusChanged(); } From ab15b8e882fa2867c86943e19184c077a0d295dc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 Nov 2021 00:14:28 +0100 Subject: [PATCH 38/73] Try to prevent leaks. --- native/Avalonia.Native/src/OSX/automation.mm | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index ca845d0ec8d..4fb4d736a65 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -30,6 +30,7 @@ - (void) raiseFocusChanged; virtual void Dispose() override { + _owner = nil; } virtual void ChildrenChanged () override @@ -48,7 +49,7 @@ virtual void FocusChanged () override } private: - AvnAccessibilityElement* _owner; + __strong AvnAccessibilityElement* _owner; }; @implementation AvnAccessibilityElement @@ -90,6 +91,13 @@ - (AvnAccessibilityElement *)initWithPeer:(IAvnAutomationPeer *)peer return self; } +- (void)dealloc +{ + if (_node) + delete _node; + _node = nullptr; +} + - (NSString *)description { return [NSString stringWithFormat:@"%@ '%@' (%p)", From af588cc193f17b2adc8b7365946a76ec43bfe735 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 Nov 2021 00:18:22 +0100 Subject: [PATCH 39/73] Stop warning. --- native/Avalonia.Native/src/OSX/automation.mm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index 4fb4d736a65..4a11f85a5a3 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -449,9 +449,13 @@ - (void)raiseFocusChanged NSAccessibilityPostNotification(focused, NSAccessibilityFocusedUIElementChangedNotification); } +// Although this method is marked as deprecated we get runtime warnings if we don't handle it. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" - (void)accessibilityPerformAction:(NSAccessibilityActionName)action { [_owner accessibilityPerformAction:action]; } +#pragma clang diagnostic pop @end From e0d37998872740f2b3f4e0d126ef2d8e92dd477e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 12 Nov 2021 14:30:52 +0100 Subject: [PATCH 40/73] Update IntegrationTestApp to net6.0. --- samples/IntegrationTestApp/IntegrationTestApp.csproj | 2 +- tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/IntegrationTestApp/IntegrationTestApp.csproj b/samples/IntegrationTestApp/IntegrationTestApp.csproj index 423b4281817..748a61bb7ea 100644 --- a/samples/IntegrationTestApp/IntegrationTestApp.csproj +++ b/samples/IntegrationTestApp/IntegrationTestApp.csproj @@ -1,7 +1,7 @@ WinExe - netcoreapp3.1 + net6.0 enable diff --git a/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs b/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs index fe2daa3cd0a..b179aea6a96 100644 --- a/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs +++ b/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs @@ -10,7 +10,7 @@ namespace Avalonia.IntegrationTests.Win32 public class TestAppFixture : IDisposable { private const string WindowsApplicationDriverUrl = "http://127.0.0.1:4723"; - private const string TestAppPath = @"..\..\..\..\..\samples\IntegrationTestApp\bin\Debug\netcoreapp3.1\IntegrationTestApp.exe"; + private const string TestAppPath = @"..\..\..\..\..\samples\IntegrationTestApp\bin\Debug\net6.0\IntegrationTestApp.exe"; public TestAppFixture() { From 77f535f49d6bff0ccbfd431c87ca796a249788ef Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 12 Nov 2021 15:34:34 +0100 Subject: [PATCH 41/73] Trying to use a shared project for win32/mac integration tests. --- .../AutomationTests.cs | 4 +- .../Avalonia.IntegrationTests.Win32.csproj | 2 +- .../ButtonTests.cs | 4 +- .../CheckBoxTests.cs | 4 +- .../ComboBoxTests.cs | 4 +- .../MenuTests.cs | 4 +- .../TestAppFixture.cs | 47 ++++++++++++++----- 7 files changed, 45 insertions(+), 24 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs b/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs index 045a4f08d1b..735214426fb 100644 --- a/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs +++ b/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs @@ -1,4 +1,4 @@ -using OpenQA.Selenium.Appium.Windows; +using OpenQA.Selenium.Appium; using Xunit; namespace Avalonia.IntegrationTests.Win32 @@ -6,7 +6,7 @@ namespace Avalonia.IntegrationTests.Win32 [Collection("Default")] public class AutomationTests { - private WindowsDriver _session; + private AppiumDriver _session; public AutomationTests(TestAppFixture fixture) { diff --git a/tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj b/tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj index f38f8b0ce16..095f0e63e07 100644 --- a/tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj +++ b/tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 enable diff --git a/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs b/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs index 21c2b1a7e3f..d44d1db1a21 100644 --- a/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs +++ b/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs @@ -1,4 +1,4 @@ -using OpenQA.Selenium.Appium.Windows; +using OpenQA.Selenium.Appium; using Xunit; namespace Avalonia.IntegrationTests.Win32 @@ -6,7 +6,7 @@ namespace Avalonia.IntegrationTests.Win32 [Collection("Default")] public class ButtonTests { - private WindowsDriver _session; + private AppiumDriver _session; public ButtonTests(TestAppFixture fixture) { diff --git a/tests/Avalonia.IntegrationTests.Win32/CheckBoxTests.cs b/tests/Avalonia.IntegrationTests.Win32/CheckBoxTests.cs index ebf7408eab4..fdc3f104155 100644 --- a/tests/Avalonia.IntegrationTests.Win32/CheckBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Win32/CheckBoxTests.cs @@ -1,4 +1,4 @@ -using OpenQA.Selenium.Appium.Windows; +using OpenQA.Selenium.Appium; using Xunit; namespace Avalonia.IntegrationTests.Win32 @@ -6,7 +6,7 @@ namespace Avalonia.IntegrationTests.Win32 [Collection("Default")] public class CheckBoxTests { - private WindowsDriver _session; + private AppiumDriver _session; public CheckBoxTests(TestAppFixture fixture) { diff --git a/tests/Avalonia.IntegrationTests.Win32/ComboBoxTests.cs b/tests/Avalonia.IntegrationTests.Win32/ComboBoxTests.cs index 6764343c02f..b920974b5c3 100644 --- a/tests/Avalonia.IntegrationTests.Win32/ComboBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Win32/ComboBoxTests.cs @@ -1,4 +1,4 @@ -using OpenQA.Selenium.Appium.Windows; +using OpenQA.Selenium.Appium; using Xunit; namespace Avalonia.IntegrationTests.Win32 @@ -6,7 +6,7 @@ namespace Avalonia.IntegrationTests.Win32 [Collection("Default")] public class ComboBoxTests { - private WindowsDriver _session; + private AppiumDriver _session; public ComboBoxTests(TestAppFixture fixture) { diff --git a/tests/Avalonia.IntegrationTests.Win32/MenuTests.cs b/tests/Avalonia.IntegrationTests.Win32/MenuTests.cs index 3d93afec128..c95bc405e86 100644 --- a/tests/Avalonia.IntegrationTests.Win32/MenuTests.cs +++ b/tests/Avalonia.IntegrationTests.Win32/MenuTests.cs @@ -1,4 +1,4 @@ -using OpenQA.Selenium.Appium.Windows; +using OpenQA.Selenium.Appium; using Xunit; namespace Avalonia.IntegrationTests.Win32 @@ -6,7 +6,7 @@ namespace Avalonia.IntegrationTests.Win32 [Collection("Default")] public class MenuTests { - private WindowsDriver _session; + private AppiumDriver _session; public MenuTests(TestAppFixture fixture) => _session = fixture.Session; diff --git a/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs b/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs index b179aea6a96..6fdc7acd96b 100644 --- a/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs +++ b/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs @@ -3,32 +3,53 @@ using System.IO; using System.Runtime.InteropServices; using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Enums; +using OpenQA.Selenium.Appium.Mac; using OpenQA.Selenium.Appium.Windows; namespace Avalonia.IntegrationTests.Win32 { public class TestAppFixture : IDisposable { - private const string WindowsApplicationDriverUrl = "http://127.0.0.1:4723"; - private const string TestAppPath = @"..\..\..\..\..\samples\IntegrationTestApp\bin\Debug\net6.0\IntegrationTestApp.exe"; + private const string TestAppPath = @"..\..\..\..\..\samples\IntegrationTestApp\bin\Debug\net6.0\IntegrationTestApp"; public TestAppFixture() { var opts = new AppiumOptions(); var path = Path.GetFullPath(TestAppPath); - opts.AddAdditionalCapability("app", path); - opts.AddAdditionalCapability("deviceName", "WindowsPC"); - Session = new WindowsDriver( - new Uri(WindowsApplicationDriverUrl), - opts); - - // https://github.com/microsoft/WinAppDriver/issues/1025 - SetForegroundWindow(new IntPtr(int.Parse( - Session.WindowHandles[0].Substring(2), - NumberStyles.AllowHexSpecifier))); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + opts.AddAdditionalCapability(MobileCapabilityType.App, path + ".exe"); + opts.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.Windows); + opts.AddAdditionalCapability(MobileCapabilityType.DeviceName, "WindowsPC"); + + Session = new WindowsDriver( + new Uri("http://127.0.0.1:4723"), + opts); + + // https://github.com/microsoft/WinAppDriver/issues/1025 + SetForegroundWindow(new IntPtr(int.Parse( + Session.WindowHandles[0].Substring(2), + NumberStyles.AllowHexSpecifier))); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + opts.AddAdditionalCapability(MobileCapabilityType.App, path + ".exe"); + opts.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.MacOS); + opts.AddAdditionalCapability(MobileCapabilityType.AutomationName, "mac2"); + + Session = new MacDriver( + new Uri("http://127.0.0.1:4723/wd/hub"), + opts); + } + else + { + throw new NotSupportedException("Unsupported platform."); + } } - public WindowsDriver Session { get; } + public AppiumDriver Session { get; } public void Dispose() => Session.Close(); From 32e76d335031d3c0b2ea16859b3a46be30083a76 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 12 Nov 2021 16:32:08 +0100 Subject: [PATCH 42/73] WIP: Bundle IntegrationTestApp. --- .../IntegrationTestApp.csproj | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/samples/IntegrationTestApp/IntegrationTestApp.csproj b/samples/IntegrationTestApp/IntegrationTestApp.csproj index 748a61bb7ea..e8338adae66 100644 --- a/samples/IntegrationTestApp/IntegrationTestApp.csproj +++ b/samples/IntegrationTestApp/IntegrationTestApp.csproj @@ -4,10 +4,24 @@ net6.0 enable - - - + + + IntegrationTestApp + net.avaloniaui.avalonia.integrationtestapp + true + 1.0.0 + + + + + + + + + + + From 9b9abb28ca1588d97d04286c31f538c2aabfaa92 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 12 Nov 2021 23:20:51 +0100 Subject: [PATCH 43/73] Added script to create bundle. --- samples/IntegrationTestApp/bundle.sh | 5 +++++ 1 file changed, 5 insertions(+) create mode 100755 samples/IntegrationTestApp/bundle.sh diff --git a/samples/IntegrationTestApp/bundle.sh b/samples/IntegrationTestApp/bundle.sh new file mode 100755 index 00000000000..505991582e8 --- /dev/null +++ b/samples/IntegrationTestApp/bundle.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +cd $(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) +dotnet restore -r osx-arm64 +dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-arm64 -p:_AvaloniaUseExternalMSBuild=false \ No newline at end of file From cab21b608430771eac07d4fdf87fca4a136fbe32 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 12 Nov 2021 23:21:56 +0100 Subject: [PATCH 44/73] Start making automation tests work on macOS. --- .../AutomationTests.cs | 2 +- .../ElementExtensions.cs | 16 ++++++++++++++++ .../TestAppFixture.cs | 7 ++++--- 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 tests/Avalonia.IntegrationTests.Win32/ElementExtensions.cs diff --git a/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs b/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs index 735214426fb..42fc019e72b 100644 --- a/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs +++ b/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs @@ -33,7 +33,7 @@ public void LabeledBy() var labeledTextBox = _session.FindElementByAccessibilityId("LabeledByTextBox"); Assert.Equal("Label for TextBox", label.Text); - Assert.Equal("Label for TextBox", labeledTextBox.GetAttribute("Name")); + Assert.Equal("Label for TextBox", labeledTextBox.GetName()); } } } diff --git a/tests/Avalonia.IntegrationTests.Win32/ElementExtensions.cs b/tests/Avalonia.IntegrationTests.Win32/ElementExtensions.cs new file mode 100644 index 00000000000..545a2741bf2 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Win32/ElementExtensions.cs @@ -0,0 +1,16 @@ +using System; +using System.Runtime.InteropServices; +using OpenQA.Selenium.Appium; + +namespace Avalonia.IntegrationTests.Win32 +{ + internal static class ElementExtensions + { + public static string GetName(this AppiumWebElement element) => GetAttribute(element, "Name", "title"); + + public static string GetAttribute(AppiumWebElement element, string windows, string macOS) + { + return element.GetAttribute(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windows : macOS); + } + } +} diff --git a/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs b/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs index 6fdc7acd96b..e8f73ec82d1 100644 --- a/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs +++ b/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs @@ -11,7 +11,8 @@ namespace Avalonia.IntegrationTests.Win32 { public class TestAppFixture : IDisposable { - private const string TestAppPath = @"..\..\..\..\..\samples\IntegrationTestApp\bin\Debug\net6.0\IntegrationTestApp"; + private const string TestAppPath = @"..\..\..\..\..\samples\IntegrationTestApp\bin\Debug\net6.0\IntegrationTestApp.exe"; + private const string TestAppBundleId = "net.avaloniaui.avalonia.integrationtestapp"; public TestAppFixture() { @@ -20,7 +21,7 @@ public TestAppFixture() if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - opts.AddAdditionalCapability(MobileCapabilityType.App, path + ".exe"); + opts.AddAdditionalCapability(MobileCapabilityType.App, path); opts.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.Windows); opts.AddAdditionalCapability(MobileCapabilityType.DeviceName, "WindowsPC"); @@ -35,7 +36,7 @@ public TestAppFixture() } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - opts.AddAdditionalCapability(MobileCapabilityType.App, path + ".exe"); + opts.AddAdditionalCapability("appium:bundleId", TestAppBundleId); opts.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.MacOS); opts.AddAdditionalCapability(MobileCapabilityType.AutomationName, "mac2"); From 71b84b3664d6bf057f830419c84d085d0d08ca38 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 13 Nov 2021 18:22:41 +0100 Subject: [PATCH 45/73] Correctly raise layout changed notification (?) --- native/Avalonia.Native/src/OSX/automation.mm | 60 +++++++++++++------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index 4a11f85a5a3..4e79d8af6e5 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -242,27 +242,7 @@ - (BOOL)isAccessibilityFocused - (NSArray *)accessibilityChildren { if (_children == nullptr && _peer != nullptr) - { - auto childPeers = _peer->GetChildren(); - auto childCount = childPeers != nullptr ? childPeers->GetCount() : 0; - - if (childCount > 0) - { - _children = [[NSMutableArray alloc] initWithCapacity:childCount]; - - for (int i = 0; i < childCount; ++i) - { - IAvnAutomationPeer* child; - - if (childPeers->Get(i, &child) == S_OK) - { - auto element = [AvnAccessibilityElement acquire:child]; - [_children addObject:element]; - } - } - } - } - + [self recalculateChildren]; return _children; } @@ -392,7 +372,17 @@ - (BOOL)isAccessibilitySelectorAllowed:(SEL)selector - (void)raiseChildrenChanged { - NSAccessibilityPostNotification(self, NSAccessibilityLayoutChangedNotification); + auto changed = _children ? [NSMutableSet setWithArray:_children] : [NSMutableSet set]; + + [self recalculateChildren]; + + if (_children) + [changed addObjectsFromArray:_children]; + + NSAccessibilityPostNotificationWithUserInfo( + self, + NSAccessibilityLayoutChangedNotification, + @{ NSAccessibilityUIElementsKey: [changed allObjects]}); } - (void)raisePropertyChanged @@ -405,6 +395,32 @@ - (void)setAccessibilityFocused:(BOOL)accessibilityFocused _peer->SetFocus(); } +- (void)recalculateChildren +{ + auto childPeers = _peer->GetChildren(); + auto childCount = childPeers != nullptr ? childPeers->GetCount() : 0; + + if (childCount > 0) + { + _children = [[NSMutableArray alloc] initWithCapacity:childCount]; + + for (int i = 0; i < childCount; ++i) + { + IAvnAutomationPeer* child; + + if (childPeers->Get(i, &child) == S_OK) + { + auto element = [AvnAccessibilityElement acquire:child]; + [_children addObject:element]; + } + } + } + else + { + _children = nil; + } +} + @end @implementation AvnRootAccessibilityElement From 812bf2665ed76281445ceba0f8866af9be0ca33a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 14 Nov 2021 00:50:14 +0100 Subject: [PATCH 46/73] Get CheckBox automation tests passing on macOS. --- native/Avalonia.Native/src/OSX/automation.mm | 8 +++-- .../CheckBoxTests.cs | 34 +++++++------------ .../ElementExtensions.cs | 9 +++++ .../TestAppFixture.cs | 12 ++++++- 4 files changed, 38 insertions(+), 25 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index 4e79d8af6e5..46515380a57 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -194,7 +194,7 @@ - (id)accessibilityValue switch (_peer->ToggleProvider_GetToggleState()) { case 0: return [NSNumber numberWithBool:NO]; case 1: return [NSNumber numberWithBool:YES]; - default: return [NSNumber numberWithInt:-1]; + default: return [NSNumber numberWithInt:2]; } } else if (_peer->IsValueProvider()) @@ -314,6 +314,10 @@ - (BOOL)accessibilityPerformPress { _peer->ExpandCollapseProvider_Expand(); } + else if (_peer->IsToggleProvider()) + { + _peer->ToggleProvider_Toggle(); + } return YES; } @@ -357,7 +361,7 @@ - (BOOL)isAccessibilitySelectorAllowed:(SEL)selector } else if (selector == @selector(accessibilityPerformPress)) { - return _peer->IsInvokeProvider() || _peer->IsExpandCollapseProvider(); + return _peer->IsInvokeProvider() || _peer->IsExpandCollapseProvider() || _peer->IsToggleProvider(); } else if (selector == @selector(accessibilityPerformIncrement) || selector == @selector(accessibilityPerformDecrement) || diff --git a/tests/Avalonia.IntegrationTests.Win32/CheckBoxTests.cs b/tests/Avalonia.IntegrationTests.Win32/CheckBoxTests.cs index fdc3f104155..71be603dc99 100644 --- a/tests/Avalonia.IntegrationTests.Win32/CheckBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Win32/CheckBoxTests.cs @@ -6,7 +6,7 @@ namespace Avalonia.IntegrationTests.Win32 [Collection("Default")] public class CheckBoxTests { - private AppiumDriver _session; + private readonly AppiumDriver _session; public CheckBoxTests(TestAppFixture fixture) { @@ -22,14 +22,11 @@ public void UncheckedCheckBox() { var checkBox = _session.FindElementByAccessibilityId("UncheckedCheckBox"); - Assert.Equal("Unchecked", checkBox.Text); - Assert.False(checkBox.Selected); - Assert.Equal("0", checkBox.GetAttribute("Toggle.ToggleState")); + Assert.Equal("Unchecked", checkBox.GetName()); + Assert.Equal(false, checkBox.GetIsChecked()); checkBox.Click(); - - Assert.True(checkBox.Selected); - Assert.Equal("1", checkBox.GetAttribute("Toggle.ToggleState")); + Assert.Equal(true, checkBox.GetIsChecked()); } [Fact] @@ -37,14 +34,11 @@ public void CheckedCheckBox() { var checkBox = _session.FindElementByAccessibilityId("CheckedCheckBox"); - Assert.Equal("Checked", checkBox.Text); - Assert.True(checkBox.Selected); - Assert.Equal("1", checkBox.GetAttribute("Toggle.ToggleState")); + Assert.Equal("Checked", checkBox.GetName()); + Assert.Equal(true, checkBox.GetIsChecked()); checkBox.Click(); - - Assert.False(checkBox.Selected); - Assert.Equal("0", checkBox.GetAttribute("Toggle.ToggleState")); + Assert.Equal(false, checkBox.GetIsChecked()); } [Fact] @@ -52,21 +46,17 @@ public void ThreeStateCheckBox() { var checkBox = _session.FindElementByAccessibilityId("ThreeStateCheckBox"); - Assert.Equal("ThreeState", checkBox.Text); - Assert.Equal("2", checkBox.GetAttribute("Toggle.ToggleState")); + Assert.Equal("ThreeState", checkBox.GetName()); + Assert.Null(checkBox.GetIsChecked()); checkBox.Click(); - - Assert.False(checkBox.Selected); - Assert.Equal("0", checkBox.GetAttribute("Toggle.ToggleState")); + Assert.Equal(false, checkBox.GetIsChecked()); checkBox.Click(); - - Assert.True(checkBox.Selected); - Assert.Equal("1", checkBox.GetAttribute("Toggle.ToggleState")); + Assert.Equal(true, checkBox.GetIsChecked()); checkBox.Click(); - Assert.Equal("2", checkBox.GetAttribute("Toggle.ToggleState")); + Assert.Null(checkBox.GetIsChecked()); } } } diff --git a/tests/Avalonia.IntegrationTests.Win32/ElementExtensions.cs b/tests/Avalonia.IntegrationTests.Win32/ElementExtensions.cs index 545a2741bf2..ac92cd0c5d6 100644 --- a/tests/Avalonia.IntegrationTests.Win32/ElementExtensions.cs +++ b/tests/Avalonia.IntegrationTests.Win32/ElementExtensions.cs @@ -8,6 +8,15 @@ internal static class ElementExtensions { public static string GetName(this AppiumWebElement element) => GetAttribute(element, "Name", "title"); + public static bool? GetIsChecked(this AppiumWebElement element) => + GetAttribute(element, "Toggle.ToggleState", "value") switch + { + "0" => false, + "1" => true, + "2" => null, + _ => throw new ArgumentOutOfRangeException($"Unexpected IsChecked value.") + }; + public static string GetAttribute(AppiumWebElement element, string windows, string macOS) { return element.GetAttribute(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windows : macOS); diff --git a/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs b/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs index e8f73ec82d1..761787872f6 100644 --- a/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs +++ b/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs @@ -52,7 +52,17 @@ public TestAppFixture() public AppiumDriver Session { get; } - public void Dispose() => Session.Close(); + public void Dispose() + { + try + { + Session.Close(); + } + catch + { + // Closing the session currently seems to crash the mac2 driver. + } + } [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] From 8250131e10a855d5ee20050d8e5f0d0a2e68eadc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 14 Nov 2021 00:52:26 +0100 Subject: [PATCH 47/73] Rename integration tests now they're not win32-only. --- Avalonia.sln | 2 +- .../AutomationTests.cs | 2 +- .../Avalonia.IntegrationTests.Appium.csproj} | 0 .../ButtonTests.cs | 2 +- .../CheckBoxTests.cs | 2 +- .../ComboBoxTests.cs | 2 +- .../DefaultCollection.cs | 2 +- .../ElementExtensions.cs | 2 +- .../MenuTests.cs | 2 +- .../Properties/AssemblyInfo.cs | 0 .../TestAppFixture.cs | 2 +- .../xunit.runner.json | 0 12 files changed, 9 insertions(+), 9 deletions(-) rename tests/{Avalonia.IntegrationTests.Win32 => Avalonia.IntegrationTests.Appium}/AutomationTests.cs (96%) rename tests/{Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj => Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj} (100%) rename tests/{Avalonia.IntegrationTests.Win32 => Avalonia.IntegrationTests.Appium}/ButtonTests.cs (97%) rename tests/{Avalonia.IntegrationTests.Win32 => Avalonia.IntegrationTests.Appium}/CheckBoxTests.cs (97%) rename tests/{Avalonia.IntegrationTests.Win32 => Avalonia.IntegrationTests.Appium}/ComboBoxTests.cs (96%) rename tests/{Avalonia.IntegrationTests.Win32 => Avalonia.IntegrationTests.Appium}/DefaultCollection.cs (76%) rename tests/{Avalonia.IntegrationTests.Win32 => Avalonia.IntegrationTests.Appium}/ElementExtensions.cs (95%) rename tests/{Avalonia.IntegrationTests.Win32 => Avalonia.IntegrationTests.Appium}/MenuTests.cs (94%) rename tests/{Avalonia.IntegrationTests.Win32 => Avalonia.IntegrationTests.Appium}/Properties/AssemblyInfo.cs (100%) rename tests/{Avalonia.IntegrationTests.Win32 => Avalonia.IntegrationTests.Appium}/TestAppFixture.cs (98%) rename tests/{Avalonia.IntegrationTests.Win32 => Avalonia.IntegrationTests.Appium}/xunit.runner.json (100%) diff --git a/Avalonia.sln b/Avalonia.sln index d8f202d38e8..f0c4ecb832e 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -232,7 +232,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniMvvm", "samples\MiniMvv EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestApp", "samples\IntegrationTestApp\IntegrationTestApp.csproj", "{676D6BFD-029D-4E43-BFC7-3892265CE251}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.IntegrationTests.Win32", "tests\Avalonia.IntegrationTests.Win32\Avalonia.IntegrationTests.Win32.csproj", "{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.IntegrationTests.Appium", "tests\Avalonia.IntegrationTests.Appium\Avalonia.IntegrationTests.Appium.csproj", "{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution diff --git a/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs b/tests/Avalonia.IntegrationTests.Appium/AutomationTests.cs similarity index 96% rename from tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs rename to tests/Avalonia.IntegrationTests.Appium/AutomationTests.cs index 42fc019e72b..38e4a70e886 100644 --- a/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/AutomationTests.cs @@ -1,7 +1,7 @@ using OpenQA.Selenium.Appium; using Xunit; -namespace Avalonia.IntegrationTests.Win32 +namespace Avalonia.IntegrationTests.Appium { [Collection("Default")] public class AutomationTests diff --git a/tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj similarity index 100% rename from tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj rename to tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj diff --git a/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs b/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs similarity index 97% rename from tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs rename to tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs index d44d1db1a21..1bae5b1069d 100644 --- a/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs @@ -1,7 +1,7 @@ using OpenQA.Selenium.Appium; using Xunit; -namespace Avalonia.IntegrationTests.Win32 +namespace Avalonia.IntegrationTests.Appium { [Collection("Default")] public class ButtonTests diff --git a/tests/Avalonia.IntegrationTests.Win32/CheckBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/CheckBoxTests.cs similarity index 97% rename from tests/Avalonia.IntegrationTests.Win32/CheckBoxTests.cs rename to tests/Avalonia.IntegrationTests.Appium/CheckBoxTests.cs index 71be603dc99..02e7ac60c41 100644 --- a/tests/Avalonia.IntegrationTests.Win32/CheckBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/CheckBoxTests.cs @@ -1,7 +1,7 @@ using OpenQA.Selenium.Appium; using Xunit; -namespace Avalonia.IntegrationTests.Win32 +namespace Avalonia.IntegrationTests.Appium { [Collection("Default")] public class CheckBoxTests diff --git a/tests/Avalonia.IntegrationTests.Win32/ComboBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs similarity index 96% rename from tests/Avalonia.IntegrationTests.Win32/ComboBoxTests.cs rename to tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs index b920974b5c3..c5ddb14d4b2 100644 --- a/tests/Avalonia.IntegrationTests.Win32/ComboBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs @@ -1,7 +1,7 @@ using OpenQA.Selenium.Appium; using Xunit; -namespace Avalonia.IntegrationTests.Win32 +namespace Avalonia.IntegrationTests.Appium { [Collection("Default")] public class ComboBoxTests diff --git a/tests/Avalonia.IntegrationTests.Win32/DefaultCollection.cs b/tests/Avalonia.IntegrationTests.Appium/DefaultCollection.cs similarity index 76% rename from tests/Avalonia.IntegrationTests.Win32/DefaultCollection.cs rename to tests/Avalonia.IntegrationTests.Appium/DefaultCollection.cs index 1c8f09a430d..bb2dd1fbecb 100644 --- a/tests/Avalonia.IntegrationTests.Win32/DefaultCollection.cs +++ b/tests/Avalonia.IntegrationTests.Appium/DefaultCollection.cs @@ -1,6 +1,6 @@ using Xunit; -namespace Avalonia.IntegrationTests.Win32 +namespace Avalonia.IntegrationTests.Appium { [CollectionDefinition("Default")] public class DefaultCollection : ICollectionFixture diff --git a/tests/Avalonia.IntegrationTests.Win32/ElementExtensions.cs b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs similarity index 95% rename from tests/Avalonia.IntegrationTests.Win32/ElementExtensions.cs rename to tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs index ac92cd0c5d6..8949aa92089 100644 --- a/tests/Avalonia.IntegrationTests.Win32/ElementExtensions.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs @@ -2,7 +2,7 @@ using System.Runtime.InteropServices; using OpenQA.Selenium.Appium; -namespace Avalonia.IntegrationTests.Win32 +namespace Avalonia.IntegrationTests.Appium { internal static class ElementExtensions { diff --git a/tests/Avalonia.IntegrationTests.Win32/MenuTests.cs b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs similarity index 94% rename from tests/Avalonia.IntegrationTests.Win32/MenuTests.cs rename to tests/Avalonia.IntegrationTests.Appium/MenuTests.cs index c95bc405e86..1e43330ae20 100644 --- a/tests/Avalonia.IntegrationTests.Win32/MenuTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs @@ -1,7 +1,7 @@ using OpenQA.Selenium.Appium; using Xunit; -namespace Avalonia.IntegrationTests.Win32 +namespace Avalonia.IntegrationTests.Appium { [Collection("Default")] public class MenuTests diff --git a/tests/Avalonia.IntegrationTests.Win32/Properties/AssemblyInfo.cs b/tests/Avalonia.IntegrationTests.Appium/Properties/AssemblyInfo.cs similarity index 100% rename from tests/Avalonia.IntegrationTests.Win32/Properties/AssemblyInfo.cs rename to tests/Avalonia.IntegrationTests.Appium/Properties/AssemblyInfo.cs diff --git a/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs b/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs similarity index 98% rename from tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs rename to tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs index 761787872f6..f1b8d5773bf 100644 --- a/tests/Avalonia.IntegrationTests.Win32/TestAppFixture.cs +++ b/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs @@ -7,7 +7,7 @@ using OpenQA.Selenium.Appium.Mac; using OpenQA.Selenium.Appium.Windows; -namespace Avalonia.IntegrationTests.Win32 +namespace Avalonia.IntegrationTests.Appium { public class TestAppFixture : IDisposable { diff --git a/tests/Avalonia.IntegrationTests.Win32/xunit.runner.json b/tests/Avalonia.IntegrationTests.Appium/xunit.runner.json similarity index 100% rename from tests/Avalonia.IntegrationTests.Win32/xunit.runner.json rename to tests/Avalonia.IntegrationTests.Appium/xunit.runner.json From 66923cdb240454fe56bcc226f03e207b6e65d2d7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 20 Nov 2021 22:38:54 +0100 Subject: [PATCH 48/73] Skip accelerator key test on OSX. OSX doesn't support accelerator keys. --- .../ButtonTests.cs | 5 ++-- .../PlatformFactAttribute.cs | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs diff --git a/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs b/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs index 1bae5b1069d..8856d7f58d5 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs @@ -1,4 +1,5 @@ -using OpenQA.Selenium.Appium; +using System.Runtime.InteropServices; +using OpenQA.Selenium.Appium; using Xunit; namespace Avalonia.IntegrationTests.Appium @@ -43,7 +44,7 @@ public void ButtonWithTextBlock() Assert.Equal("Button with TextBlock", button.Text); } - [Fact] + [PlatformFact(SkipOnOSX = true)] public void ButtonWithAcceleratorKey() { var button = _session.FindElementByAccessibilityId("ButtonWithAcceleratorKey"); diff --git a/tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs b/tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs new file mode 100644 index 00000000000..53b9a983474 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs @@ -0,0 +1,25 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + internal class PlatformFactAttribute : FactAttribute + { + public override string? Skip + { + get + { + if (SkipOnWindows && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return "Ignored on Windows"; + if (SkipOnOSX && RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return "Ignored on Windows"; + return null; + } + set => throw new NotSupportedException(); + } + public bool SkipOnOSX { get; set; } + public bool SkipOnWindows { get; set; } + } +} From 6b177078126827b2093330a59073eee3709eac76 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 20 Nov 2021 23:31:47 +0100 Subject: [PATCH 49/73] Raise focus changed on new window if it has focus. --- native/Avalonia.Native/src/OSX/automation.mm | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index 46515380a57..556cf209af2 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -436,6 +436,14 @@ - (AvnRootAccessibilityElement *)initWithPeer:(IAvnAutomationPeer *)peer owner:( { self = [super initWithPeer:peer]; _owner = owner; + + // Seems we need to raise a focus changed notification here if we have focus + auto focusedPeer = [self peer]->RootProvider_GetFocus(); + id focused = [AvnAccessibilityElement acquire:focusedPeer]; + + if (focused) + NSAccessibilityPostNotification(focused, NSAccessibilityFocusedUIElementChangedNotification); + return self; } From f4d9d4cc131ba0d14122fad6f413eeee5644dc0f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 21 Nov 2021 15:33:33 +0100 Subject: [PATCH 50/73] Make integration tests pass on MacOS. --- .../Peers/ComboBoxAutomationPeer.cs | 20 ++++++++++++++++--- .../ComboBoxTests.cs | 5 +++-- .../ElementExtensions.cs | 19 ++++++++++++++++++ .../MenuTests.cs | 2 +- .../PlatformFactAttribute.cs | 2 +- 5 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs index c582c3d3724..01225279501 100644 --- a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Automation.Provider; using Avalonia.Controls; @@ -7,12 +8,13 @@ namespace Avalonia.Automation.Peers { public class ComboBoxAutomationPeer : SelectingItemsControlAutomationPeer, - IExpandCollapseProvider + IExpandCollapseProvider, + IValueProvider { private UnrealizedSelectionPeer[]? _selection; public ComboBoxAutomationPeer(ComboBox owner) - : base(owner) + : base(owner) { } @@ -22,7 +24,19 @@ public ComboBoxAutomationPeer(ComboBox owner) public bool ShowsMenu => true; public void Collapse() => Owner.IsDropDownOpen = false; public void Expand() => Owner.IsDropDownOpen = true; + bool IValueProvider.IsReadOnly => true; + string? IValueProvider.Value + { + get + { + var selection = GetSelection(); + return selection.Count == 1 ? selection[0].GetName() : null; + } + } + + void IValueProvider.SetValue(string? value) => throw new NotSupportedException(); + protected override AutomationControlType GetAutomationControlTypeCore() { return AutomationControlType.ComboBox; diff --git a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs index c5ddb14d4b2..f8920e62904 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs @@ -1,4 +1,5 @@ using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Mac; using Xunit; namespace Avalonia.IntegrationTests.Appium @@ -24,8 +25,8 @@ public void UnselectedComboBox() Assert.Equal(string.Empty, comboBox.Text); - comboBox.Click(); - comboBox.FindElementByName("Bar").Click(); + ((MacElement)comboBox).Click(); + _session.FindElementByName("Bar").ClickListItem(); Assert.Equal("Bar", comboBox.Text); } diff --git a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs index 8949aa92089..9e91422c383 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs @@ -1,6 +1,8 @@ using System; using System.Runtime.InteropServices; using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.MultiTouch; +using OpenQA.Selenium.Interactions; namespace Avalonia.IntegrationTests.Appium { @@ -17,6 +19,23 @@ internal static class ElementExtensions _ => throw new ArgumentOutOfRangeException($"Unexpected IsChecked value.") }; + public static void ClickListItem(this AppiumWebElement element) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + element.Click(); + } + else + { + // List items don't respond to performClick on MacOS, so instead send a physical click as VoiceOver + // does. + var action = new Actions(element.WrappedDriver); + action.MoveToElement(element); + action.Click(); + action.Perform(); + } + } + public static string GetAttribute(AppiumWebElement element, string windows, string macOS) { return element.GetAttribute(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windows : macOS); diff --git a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs index 1e43330ae20..a2c3ee0e441 100644 --- a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs @@ -18,7 +18,7 @@ public void File() Assert.Equal("File", fileMenu.Text); } - [Fact] + [PlatformFact(SkipOnOSX = true)] public void Open() { var fileMenu = _session.FindElementByAccessibilityId("FileMenu"); diff --git a/tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs b/tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs index 53b9a983474..60338b92c23 100644 --- a/tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs +++ b/tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs @@ -14,7 +14,7 @@ public override string? Skip if (SkipOnWindows && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return "Ignored on Windows"; if (SkipOnOSX && RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - return "Ignored on Windows"; + return "Ignored on MacOS"; return null; } set => throw new NotSupportedException(); From dc83f8e788f7b70681b306dfdccf94679ebddd7f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 21 Nov 2021 16:49:43 +0100 Subject: [PATCH 51/73] Use ListItemAutomationPeer for TabItem. --- src/Avalonia.Controls/TabItem.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 7f3df0ed8a5..f68db4743bb 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -56,6 +56,8 @@ public bool IsSelected set { SetValue(IsSelectedProperty, value); } } + protected override AutomationPeer OnCreateAutomationPeer() => new ListItemAutomationPeer(this); + private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj) { if (Header == null) From 5534e66bcddb8aa12a2d57c3cd698f6e6ec77105 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 21 Nov 2021 16:50:47 +0100 Subject: [PATCH 52/73] Implement isAccessibilitySelected. --- native/Avalonia.Native/src/OSX/automation.mm | 7 +++++++ src/Avalonia.Native/AvnAutomationPeer.cs | 3 +++ src/Avalonia.Native/avn.idl | 3 +++ 3 files changed, 13 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index 556cf209af2..7d697140c23 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -349,6 +349,13 @@ - (BOOL)accessibilityPerformShowMenu return YES; } +- (BOOL)isAccessibilitySelected +{ + if (_peer->IsSelectionItemProvider()) + return _peer->SelectionItemProvider_IsSelected(); + return NO; +} + - (BOOL)isAccessibilitySelectorAllowed:(SEL)selector { if (selector == @selector(accessibilityPerformShowMenu)) diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index bcb3d54fcfb..4c1e69aa16b 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -126,6 +126,9 @@ public IAvnWindowBase RootProvider_GetWindow() public double RangeValueProvider_GetSmallChange() => ((IRangeValueProvider)_inner).SmallChange; public double RangeValueProvider_GetLargeChange() => ((IRangeValueProvider)_inner).LargeChange; public void RangeValueProvider_SetValue(double value) => ((IRangeValueProvider)_inner).SetValue(value); + + public int IsSelectionItemProvider() => (_inner is ISelectionItemProvider).AsComBool(); + public int SelectionItemProvider_IsSelected() => ((ISelectionItemProvider)_inner).IsSelected.AsComBool(); public int IsToggleProvider() => (_inner is IToggleProvider).AsComBool(); public int ToggleProvider_GetToggleState() => (int)((IToggleProvider)_inner).ToggleState; diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 954aeb3fb42..6bb6f69a6f8 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -856,6 +856,9 @@ interface IAvnAutomationPeer : IUnknown double RangeValueProvider_GetLargeChange(); void RangeValueProvider_SetValue(double value); + bool IsSelectionItemProvider(); + bool SelectionItemProvider_IsSelected(); + bool IsToggleProvider(); int ToggleProvider_GetToggleState(); void ToggleProvider_Toggle(); From 610ef6f467cf2b7fa0c7c590c26cc074f37cce60 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 21 Nov 2021 16:52:13 +0100 Subject: [PATCH 53/73] Added a NativeMenu test for macOS. --- samples/IntegrationTestApp/MainWindow.axaml | 18 ++++++--- .../IntegrationTestApp/MainWindow.axaml.cs | 21 +++++++++++ .../AutomationTests.cs | 2 +- .../ButtonTests.cs | 2 +- .../ComboBoxTests.cs | 2 +- .../MenuTests.cs | 4 +- .../NativeMenuTests.cs | 37 +++++++++++++++++++ 7 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index c72d45783c4..57f8971aaba 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -5,12 +5,20 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="IntegrationTestApp.MainWindow" Title="IntegrationTestApp"> + + + + + + + + + + + + - - - - - + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 1d3ca28432c..5cff7ccdbc5 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -1,3 +1,4 @@ +using System; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; @@ -9,6 +10,7 @@ public class MainWindow : Window public MainWindow() { InitializeComponent(); + InitializeViewMenu(); this.AttachDevTools(); } @@ -16,5 +18,24 @@ private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } + + private void InitializeViewMenu() + { + var mainTabs = this.FindControl("MainTabs"); + var viewMenu = (NativeMenuItem)NativeMenu.GetMenu(this).Items[1]; + + foreach (TabItem tabItem in mainTabs.Items) + { + var menuItem = new NativeMenuItem + { + Header = (string)tabItem.Header!, + IsChecked = tabItem.IsSelected, + ToggleType = NativeMenuItemToggleType.Radio, + }; + + menuItem.Click += (s, e) => tabItem.IsSelected = true; + viewMenu.Menu.Items.Add(menuItem); + } + } } } diff --git a/tests/Avalonia.IntegrationTests.Appium/AutomationTests.cs b/tests/Avalonia.IntegrationTests.Appium/AutomationTests.cs index 38e4a70e886..bad015506f9 100644 --- a/tests/Avalonia.IntegrationTests.Appium/AutomationTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/AutomationTests.cs @@ -6,7 +6,7 @@ namespace Avalonia.IntegrationTests.Appium [Collection("Default")] public class AutomationTests { - private AppiumDriver _session; + private readonly AppiumDriver _session; public AutomationTests(TestAppFixture fixture) { diff --git a/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs b/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs index 8856d7f58d5..2ac859e091f 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs @@ -7,7 +7,7 @@ namespace Avalonia.IntegrationTests.Appium [Collection("Default")] public class ButtonTests { - private AppiumDriver _session; + private readonly AppiumDriver _session; public ButtonTests(TestAppFixture fixture) { diff --git a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs index f8920e62904..746a919162f 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs @@ -7,7 +7,7 @@ namespace Avalonia.IntegrationTests.Appium [Collection("Default")] public class ComboBoxTests { - private AppiumDriver _session; + private readonly AppiumDriver _session; public ComboBoxTests(TestAppFixture fixture) { diff --git a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs index a2c3ee0e441..4a0e5d76ba2 100644 --- a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs @@ -6,7 +6,7 @@ namespace Avalonia.IntegrationTests.Appium [Collection("Default")] public class MenuTests { - private AppiumDriver _session; + private readonly AppiumDriver _session; public MenuTests(TestAppFixture fixture) => _session = fixture.Session; @@ -19,7 +19,7 @@ public void File() } [PlatformFact(SkipOnOSX = true)] - public void Open() + public void OpenMenu_AcceleratorKey() { var fileMenu = _session.FindElementByAccessibilityId("FileMenu"); fileMenu.Click(); diff --git a/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs b/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs new file mode 100644 index 00000000000..fde01f0e414 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs @@ -0,0 +1,37 @@ +using OpenQA.Selenium.Appium; +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + [Collection("Default")] + public class NativeMenuTests + { + private readonly AppiumDriver _session; + + public NativeMenuTests(TestAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("Automation"); + tab.Click(); + } + + [PlatformFact(SkipOnWindows = true)] + public void View_Menu_Select_Button_Tab() + { + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var buttonTab = tabs.FindElementByName("Button"); + var menuBar = _session.FindElementByXPath("/XCUIElementTypeApplication/XCUIElementTypeMenuBar"); + var viewMenu = menuBar.FindElementByName("View"); + + Assert.False(buttonTab.Selected); + + viewMenu.Click(); + var buttonMenu = viewMenu.FindElementByName("Button"); + buttonMenu.Click(); + + Assert.True(buttonTab.Selected); + } + } +} From f6e06a6d15bb8c18a005920197f9f89add29a3c0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 21 Nov 2021 17:38:40 +0100 Subject: [PATCH 54/73] Refactored Menu integration tests. --- samples/IntegrationTestApp/MainWindow.axaml | 14 ++++++ .../IntegrationTestApp/MainWindow.axaml.cs | 7 +++ .../ComboBoxTests.cs | 2 +- .../ElementExtensions.cs | 5 +- .../MenuTests.cs | 50 +++++++++++++++---- 5 files changed, 66 insertions(+), 12 deletions(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 57f8971aaba..b33b2c21e03 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -71,6 +71,20 @@ + + + + + 0 + + + + + + + None + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 5cff7ccdbc5..8079472ad87 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -1,6 +1,7 @@ using System; using Avalonia; using Avalonia.Controls; +using Avalonia.Interactivity; using Avalonia.Markup.Xaml; namespace IntegrationTestApp @@ -37,5 +38,11 @@ private void InitializeViewMenu() viewMenu.Menu.Items.Add(menuItem); } } + + private void MenuClicked(object? sender, RoutedEventArgs e) + { + var clickedMenuItemTextBlock = this.FindControl("ClickedMenuItem"); + clickedMenuItemTextBlock.Text = ((MenuItem)sender!).Header.ToString(); + } } } diff --git a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs index 746a919162f..cc72c5ce882 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs @@ -26,7 +26,7 @@ public void UnselectedComboBox() Assert.Equal(string.Empty, comboBox.Text); ((MacElement)comboBox).Click(); - _session.FindElementByName("Bar").ClickListItem(); + _session.FindElementByName("Bar").SendClick(); Assert.Equal("Bar", comboBox.Text); } diff --git a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs index 9e91422c383..0d31e1de272 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs @@ -19,7 +19,7 @@ internal static class ElementExtensions _ => throw new ArgumentOutOfRangeException($"Unexpected IsChecked value.") }; - public static void ClickListItem(this AppiumWebElement element) + public static void SendClick(this AppiumWebElement element) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -27,7 +27,8 @@ public static void ClickListItem(this AppiumWebElement element) } else { - // List items don't respond to performClick on MacOS, so instead send a physical click as VoiceOver + // The Click() method seems to correspond to accessibilityPerformPress on macOS but certain controls + // such as list items don't support this action, so instead simulate a physical click as VoiceOver // does. var action = new Actions(element.WrappedDriver); action.MoveToElement(element); diff --git a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs index 4a0e5d76ba2..e9a433b9754 100644 --- a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs @@ -8,24 +8,56 @@ public class MenuTests { private readonly AppiumDriver _session; - public MenuTests(TestAppFixture fixture) => _session = fixture.Session; + public MenuTests(TestAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("Menu"); + tab.Click(); + } + + [Fact] + public void Click_Child() + { + var rootMenuItem = _session.FindElementByAccessibilityId("RootMenuItem"); + + rootMenuItem.SendClick(); + + var childMenuItem = _session.FindElementByAccessibilityId("Child1MenuItem"); + childMenuItem.SendClick(); + + var clickedMenuItem = _session.FindElementByAccessibilityId("ClickedMenuItem"); + Assert.Equal("_Child 1", clickedMenuItem.Text); + } [Fact] - public void File() + public void Click_Grandchild() { - var fileMenu = _session.FindElementByAccessibilityId("FileMenu"); + var rootMenuItem = _session.FindElementByAccessibilityId("RootMenuItem"); + + rootMenuItem.SendClick(); - Assert.Equal("File", fileMenu.Text); + var childMenuItem = _session.FindElementByAccessibilityId("Child2MenuItem"); + childMenuItem.SendClick(); + + var grandchildMenuItem = _session.FindElementByAccessibilityId("GrandchildMenuItem"); + grandchildMenuItem.SendClick(); + + var clickedMenuItem = _session.FindElementByAccessibilityId("ClickedMenuItem"); + Assert.Equal("_Grandchild", clickedMenuItem.Text); } [PlatformFact(SkipOnOSX = true)] - public void OpenMenu_AcceleratorKey() + public void Child_AcceleratorKey() { - var fileMenu = _session.FindElementByAccessibilityId("FileMenu"); - fileMenu.Click(); + var rootMenuItem = _session.FindElementByAccessibilityId("RootMenuItem"); + + rootMenuItem.SendClick(); + + var childMenuItem = _session.FindElementByAccessibilityId("Child1MenuItem"); - var openMenu = fileMenu.FindElementByAccessibilityId("OpenMenu"); - Assert.Equal("Ctrl+O", openMenu.GetAttribute("AcceleratorKey")); + Assert.Equal("Ctrl+O", childMenuItem.GetAttribute("AcceleratorKey")); } } } From 742d9608746f7f554035613c3ef011f400ccb48e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 21 Nov 2021 22:13:38 +0100 Subject: [PATCH 55/73] Remove rogue character. --- samples/IntegrationTestApp/MainWindow.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index b33b2c21e03..035e0e211bf 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -75,7 +75,7 @@ - 0 + From 33ca12c48c411c2c929274170a0a18ea7df74e19 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 21 Nov 2021 22:17:19 +0100 Subject: [PATCH 56/73] Remove faulty cast. --- tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs index cc72c5ce882..26cbd1127f5 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs @@ -25,7 +25,7 @@ public void UnselectedComboBox() Assert.Equal(string.Empty, comboBox.Text); - ((MacElement)comboBox).Click(); + comboBox.Click(); _session.FindElementByName("Bar").SendClick(); Assert.Equal("Bar", comboBox.Text); From ddb6ab977e796c402f1535e141d41b369029da44 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 22 Nov 2021 14:25:37 +0100 Subject: [PATCH 57/73] Refactored ComboBox integration tests. --- samples/IntegrationTestApp/MainWindow.axaml | 18 ++--- .../IntegrationTestApp/MainWindow.axaml.cs | 11 +++ .../ComboBoxTests.cs | 74 ++++++++++++++++--- 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 035e0e211bf..b0483cba328 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -19,6 +19,7 @@ + @@ -32,6 +33,7 @@ + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 8079472ad87..e61341c1593 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -13,6 +13,7 @@ public MainWindow() InitializeComponent(); InitializeViewMenu(); this.AttachDevTools(); + AddHandler(Button.ClickEvent, OnButtonClick); } private void InitializeComponent() @@ -44,5 +45,15 @@ private void MenuClicked(object? sender, RoutedEventArgs e) var clickedMenuItemTextBlock = this.FindControl("ClickedMenuItem"); clickedMenuItemTextBlock.Text = ((MenuItem)sender!).Header.ToString(); } + + private void OnButtonClick(object? sender, RoutedEventArgs e) + { + var source = e.Source as Button; + + if (source?.Name == "ComboBoxSelectionClear") + this.FindControl("ComboBox").SelectedIndex = -1; + if (source?.Name == "ComboBoxSelectFirst") + this.FindControl("ComboBox").SelectedIndex = 0; + } } } diff --git a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs index 26cbd1127f5..9c458059cfa 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs @@ -1,4 +1,6 @@ -using OpenQA.Selenium.Appium; +using System.Threading; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Mac; using Xunit; @@ -19,32 +21,82 @@ public ComboBoxTests(TestAppFixture fixture) } [Fact] - public void UnselectedComboBox() + public void Can_Change_Selection_Using_Mouse() { - var comboBox = _session.FindElementByAccessibilityId("UnselectedComboBox"); + var comboBox = _session.FindElementByAccessibilityId("ComboBox"); + _session.FindElementByAccessibilityId("ComboBoxSelectFirst").Click(); + Assert.Equal("Item 0", comboBox.Text); + + comboBox.Click(); + _session.FindElementByName("Item 1").SendClick(); + + Assert.Equal("Item 1", comboBox.Text); + } + + [Fact] + public void Can_Change_Selection_From_Unselected_Using_Mouse() + { + var comboBox = _session.FindElementByAccessibilityId("ComboBox"); + + _session.FindElementByAccessibilityId("ComboBoxSelectionClear").Click(); Assert.Equal(string.Empty, comboBox.Text); comboBox.Click(); - _session.FindElementByName("Bar").SendClick(); + _session.FindElementByName("Item 0").SendClick(); - Assert.Equal("Bar", comboBox.Text); + Assert.Equal("Item 0", comboBox.Text); } [Fact] - public void SelectedIndex0ComboBox() + public void Can_Change_Selection_With_Keyboard() { - var comboBox = _session.FindElementByAccessibilityId("SelectedIndex0ComboBox"); + var comboBox = _session.FindElementByAccessibilityId("ComboBox"); + + _session.FindElementByAccessibilityId("ComboBoxSelectFirst").Click(); + Assert.Equal("Item 0", comboBox.Text); + + comboBox.SendKeys(Keys.LeftAlt + Keys.ArrowDown); + comboBox.SendKeys(Keys.ArrowDown); - Assert.Equal("Foo", comboBox.Text); + var item = _session.FindElementByName("Item 1"); + item.SendKeys(Keys.Enter); + + Assert.Equal("Item 1", comboBox.Text); } [Fact] - public void SelectedIndex1ComboBox() + public void Can_Change_Selection_With_Keyboard_From_Unselected() { - var comboBox = _session.FindElementByAccessibilityId("SelectedIndex1ComboBox"); + var comboBox = _session.FindElementByAccessibilityId("ComboBox"); + + _session.FindElementByAccessibilityId("ComboBoxSelectionClear").Click(); + Assert.Equal(string.Empty, comboBox.Text); - Assert.Equal("Bar", comboBox.Text); + comboBox.SendKeys(Keys.LeftAlt + Keys.ArrowDown); + comboBox.SendKeys(Keys.ArrowDown); + + var item = _session.FindElementByName("Item 0"); + item.SendKeys(Keys.Enter); + + Assert.Equal("Item 0", comboBox.Text); + } + + [Fact] + public void Can_Cancel_Keyboard_Selection_With_Escape() + { + var comboBox = _session.FindElementByAccessibilityId("ComboBox"); + + _session.FindElementByAccessibilityId("ComboBoxSelectionClear").Click(); + Assert.Equal(string.Empty, comboBox.Text); + + comboBox.SendKeys(Keys.LeftAlt + Keys.ArrowDown); + comboBox.SendKeys(Keys.ArrowDown); + + var item = _session.FindElementByName("Item 0"); + item.SendKeys(Keys.Escape); + + Assert.Equal(string.Empty, comboBox.Text); } } } From 26137caacaa2753f6f6ce4593967ea020ced208d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 22 Nov 2021 14:59:41 +0100 Subject: [PATCH 58/73] Trying to get stuff working on macOS. Man, appium for mac sucks. --- samples/IntegrationTestApp/MainWindow.axaml | 2 +- .../IntegrationTestApp/MainWindow.axaml.cs | 4 +-- .../ComboBoxTests.cs | 30 +++++++++---------- .../ElementExtensions.cs | 7 +++++ .../TestAppFixture.cs | 1 + 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index b0483cba328..90407294b54 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -59,7 +59,7 @@ - + Item 0 Item 1 diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index e61341c1593..74bfb6b5796 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -51,9 +51,9 @@ private void OnButtonClick(object? sender, RoutedEventArgs e) var source = e.Source as Button; if (source?.Name == "ComboBoxSelectionClear") - this.FindControl("ComboBox").SelectedIndex = -1; + this.FindControl("BasicComboBox").SelectedIndex = -1; if (source?.Name == "ComboBoxSelectFirst") - this.FindControl("ComboBox").SelectedIndex = 0; + this.FindControl("BasicComboBox").SelectedIndex = 0; } } } diff --git a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs index 9c458059cfa..677ba02b953 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs @@ -23,38 +23,38 @@ public ComboBoxTests(TestAppFixture fixture) [Fact] public void Can_Change_Selection_Using_Mouse() { - var comboBox = _session.FindElementByAccessibilityId("ComboBox"); + var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); _session.FindElementByAccessibilityId("ComboBoxSelectFirst").Click(); - Assert.Equal("Item 0", comboBox.Text); + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); comboBox.Click(); _session.FindElementByName("Item 1").SendClick(); - Assert.Equal("Item 1", comboBox.Text); + Assert.Equal("Item 1", comboBox.GetComboBoxValue()); } [Fact] public void Can_Change_Selection_From_Unselected_Using_Mouse() { - var comboBox = _session.FindElementByAccessibilityId("ComboBox"); + var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); _session.FindElementByAccessibilityId("ComboBoxSelectionClear").Click(); - Assert.Equal(string.Empty, comboBox.Text); + Assert.Equal(string.Empty, comboBox.GetComboBoxValue()); comboBox.Click(); _session.FindElementByName("Item 0").SendClick(); - Assert.Equal("Item 0", comboBox.Text); + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); } [Fact] public void Can_Change_Selection_With_Keyboard() { - var comboBox = _session.FindElementByAccessibilityId("ComboBox"); + var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); _session.FindElementByAccessibilityId("ComboBoxSelectFirst").Click(); - Assert.Equal("Item 0", comboBox.Text); + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); comboBox.SendKeys(Keys.LeftAlt + Keys.ArrowDown); comboBox.SendKeys(Keys.ArrowDown); @@ -62,16 +62,16 @@ public void Can_Change_Selection_With_Keyboard() var item = _session.FindElementByName("Item 1"); item.SendKeys(Keys.Enter); - Assert.Equal("Item 1", comboBox.Text); + Assert.Equal("Item 1", comboBox.GetComboBoxValue()); } [Fact] public void Can_Change_Selection_With_Keyboard_From_Unselected() { - var comboBox = _session.FindElementByAccessibilityId("ComboBox"); + var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); _session.FindElementByAccessibilityId("ComboBoxSelectionClear").Click(); - Assert.Equal(string.Empty, comboBox.Text); + Assert.Equal(string.Empty, comboBox.GetComboBoxValue()); comboBox.SendKeys(Keys.LeftAlt + Keys.ArrowDown); comboBox.SendKeys(Keys.ArrowDown); @@ -79,16 +79,16 @@ public void Can_Change_Selection_With_Keyboard_From_Unselected() var item = _session.FindElementByName("Item 0"); item.SendKeys(Keys.Enter); - Assert.Equal("Item 0", comboBox.Text); + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); } [Fact] public void Can_Cancel_Keyboard_Selection_With_Escape() { - var comboBox = _session.FindElementByAccessibilityId("ComboBox"); + var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); _session.FindElementByAccessibilityId("ComboBoxSelectionClear").Click(); - Assert.Equal(string.Empty, comboBox.Text); + Assert.Equal(string.Empty, comboBox.GetComboBoxValue()); comboBox.SendKeys(Keys.LeftAlt + Keys.ArrowDown); comboBox.SendKeys(Keys.ArrowDown); @@ -96,7 +96,7 @@ public void Can_Cancel_Keyboard_Selection_With_Escape() var item = _session.FindElementByName("Item 0"); item.SendKeys(Keys.Escape); - Assert.Equal(string.Empty, comboBox.Text); + Assert.Equal(string.Empty, comboBox.GetComboBoxValue()); } } } diff --git a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs index 0d31e1de272..81d85be861d 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs @@ -8,6 +8,13 @@ namespace Avalonia.IntegrationTests.Appium { internal static class ElementExtensions { + public static string GetComboBoxValue(this AppiumWebElement element) + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? + element.Text : + element.GetAttribute("value"); + } + public static string GetName(this AppiumWebElement element) => GetAttribute(element, "Name", "title"); public static bool? GetIsChecked(this AppiumWebElement element) => diff --git a/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs b/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs index f1b8d5773bf..b3385d8ee73 100644 --- a/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs +++ b/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs @@ -39,6 +39,7 @@ public TestAppFixture() opts.AddAdditionalCapability("appium:bundleId", TestAppBundleId); opts.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.MacOS); opts.AddAdditionalCapability(MobileCapabilityType.AutomationName, "mac2"); + opts.AddAdditionalCapability("appium:showServerLogs", true); Session = new MacDriver( new Uri("http://127.0.0.1:4723/wd/hub"), From b4a183bd9746a33c5ab12a37c5cda3bc0d12c924 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 22 Nov 2021 15:12:36 +0100 Subject: [PATCH 59/73] Skip keyboard interaction tests on macOS. They send appium for mac crazy. --- tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs index 677ba02b953..4f407e5964d 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs @@ -48,7 +48,7 @@ public void Can_Change_Selection_From_Unselected_Using_Mouse() Assert.Equal("Item 0", comboBox.GetComboBoxValue()); } - [Fact] + [PlatformFact(SkipOnOSX = true)] public void Can_Change_Selection_With_Keyboard() { var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); @@ -65,7 +65,7 @@ public void Can_Change_Selection_With_Keyboard() Assert.Equal("Item 1", comboBox.GetComboBoxValue()); } - [Fact] + [PlatformFact(SkipOnOSX = true)] public void Can_Change_Selection_With_Keyboard_From_Unselected() { var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); @@ -82,7 +82,7 @@ public void Can_Change_Selection_With_Keyboard_From_Unselected() Assert.Equal("Item 0", comboBox.GetComboBoxValue()); } - [Fact] + [PlatformFact(SkipOnOSX = true)] public void Can_Cancel_Keyboard_Selection_With_Escape() { var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); From 261f46ea742b4e3bcff716550397d9cd360a5002 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 23 Nov 2021 10:42:35 +0100 Subject: [PATCH 60/73] Added a few ListBox tests. --- samples/IntegrationTestApp/MainWindow.axaml | 9 ++ .../IntegrationTestApp/MainWindow.axaml.cs | 8 ++ .../ComboBoxTests.cs | 4 +- .../ElementExtensions.cs | 10 +- .../ListBoxTests.cs | 103 ++++++++++++++++++ 5 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 90407294b54..fe1487a7f2a 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -67,6 +67,15 @@ + + + + + + + + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 74bfb6b5796..b9e631a3125 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; @@ -14,8 +16,12 @@ public MainWindow() InitializeViewMenu(); this.AttachDevTools(); AddHandler(Button.ClickEvent, OnButtonClick); + ListBoxItems = Enumerable.Range(0, 100).Select(x => "Item " + x).ToList(); + DataContext = this; } + public List ListBoxItems { get; } + private void InitializeComponent() { AvaloniaXamlLoader.Load(this); @@ -54,6 +60,8 @@ private void OnButtonClick(object? sender, RoutedEventArgs e) this.FindControl("BasicComboBox").SelectedIndex = -1; if (source?.Name == "ComboBoxSelectFirst") this.FindControl("BasicComboBox").SelectedIndex = 0; + if (source?.Name == "ListBoxSelectionClear") + this.FindControl("BasicListBox").SelectedIndex = -1; } } } diff --git a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs index 4f407e5964d..fad3e1eb9f2 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs @@ -1,7 +1,5 @@ -using System.Threading; -using OpenQA.Selenium; +using OpenQA.Selenium; using OpenQA.Selenium.Appium; -using OpenQA.Selenium.Appium.Mac; using Xunit; namespace Avalonia.IntegrationTests.Appium diff --git a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs index 81d85be861d..15e22f44241 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs @@ -1,13 +1,16 @@ using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using OpenQA.Selenium.Appium; -using OpenQA.Selenium.Appium.MultiTouch; using OpenQA.Selenium.Interactions; namespace Avalonia.IntegrationTests.Appium { internal static class ElementExtensions { + public static IReadOnlyList GetChildren(this AppiumWebElement element) => + element.FindElementsByXPath("*/*"); + public static string GetComboBoxValue(this AppiumWebElement element) { return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @@ -37,10 +40,7 @@ public static void SendClick(this AppiumWebElement element) // The Click() method seems to correspond to accessibilityPerformPress on macOS but certain controls // such as list items don't support this action, so instead simulate a physical click as VoiceOver // does. - var action = new Actions(element.WrappedDriver); - action.MoveToElement(element); - action.Click(); - action.Perform(); + new Actions(element.WrappedDriver).MoveToElement(element).Click().Perform(); } } diff --git a/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs new file mode 100644 index 00000000000..9c2252e99f2 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs @@ -0,0 +1,103 @@ +using System.Threading; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Interactions; +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + [Collection("Default")] + public class ListBoxTests + { + private readonly AppiumDriver _session; + + public ListBoxTests(TestAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("ListBox"); + tab.Click(); + } + + [Fact] + public void Can_Select_Item_By_Clicking() + { + var listBox = GetTarget(); + var item2 = listBox.FindElementByName("Item 2"); + var item4 = listBox.FindElementByName("Item 4"); + + Assert.False(item2.Selected); + Assert.False(item4.Selected); + + item2.SendClick(); + Assert.True(item2.Selected); + Assert.False(item4.Selected); + + item4.SendClick(); + Assert.False(item2.Selected); + Assert.True(item4.Selected); + } + + // WinAppDriver seems unable to consistently send a Ctrl key. + [PlatformFact(SkipOnWindows = true)] + public void Can_Select_Items_By_Ctrl_Clicking() + { + var listBox = GetTarget(); + var item2 = listBox.FindElementByName("Item 2"); + var item4 = listBox.FindElementByName("Item 4"); + + Assert.False(item2.Selected); + Assert.False(item4.Selected); + + new Actions(_session) + .Click(item2) + .KeyDown(Keys.Control) + .Click(item4) + .KeyUp(Keys.Control) + .Perform(); + + Assert.True(item2.Selected); + Assert.True(item4.Selected); + } + + [Fact] + public void Can_Select_Range_By_Shift_Clicking() + { + var listBox = GetTarget(); + var item2 = listBox.FindElementByName("Item 2"); + var item3 = listBox.FindElementByName("Item 3"); + var item4 = listBox.FindElementByName("Item 4"); + + Assert.False(item2.Selected); + Assert.False(item3.Selected); + Assert.False(item4.Selected); + + new Actions(_session) + .Click(item2) + .KeyDown(Keys.Shift) + .Click(item4) + .KeyUp(Keys.Shift) + .Perform(); + + Assert.True(item2.Selected); + Assert.True(item3.Selected); + Assert.True(item4.Selected); + } + + [Fact] + public void Is_Virtualized() + { + var listBox = GetTarget(); + var children = listBox.GetChildren(); + + Assert.True(children.Count < 100); + } + + private AppiumWebElement GetTarget() + { + _session.FindElementByAccessibilityId("ListBoxSelectionClear").Click(); + return _session.FindElementByAccessibilityId("BasicListBox"); + } + } +} From 9f807c9737e2c51bf78637c9d083b77cc730c711 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 23 Nov 2021 10:53:37 +0100 Subject: [PATCH 61/73] Sigh. Appium sucks. --- tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs index 9c2252e99f2..625742ac202 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs @@ -39,8 +39,7 @@ public void Can_Select_Item_By_Clicking() Assert.True(item4.Selected); } - // WinAppDriver seems unable to consistently send a Ctrl key. - [PlatformFact(SkipOnWindows = true)] + [Fact(Skip = "WinAppDriver seems unable to consistently send a Ctrl key and appium-mac2-driver just hangs")] public void Can_Select_Items_By_Ctrl_Clicking() { var listBox = GetTarget(); @@ -61,7 +60,8 @@ public void Can_Select_Items_By_Ctrl_Clicking() Assert.True(item4.Selected); } - [Fact] + // appium-mac2-driver just hangs + [PlatformFact(SkipOnOSX = true)] public void Can_Select_Range_By_Shift_Clicking() { var listBox = GetTarget(); From 6da59f9eee2da945ca99ce929990583eef1652fb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 23 Nov 2021 16:57:27 +0100 Subject: [PATCH 62/73] Added integration tests readme. --- .../readme.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tests/Avalonia.IntegrationTests.Appium/readme.md diff --git a/tests/Avalonia.IntegrationTests.Appium/readme.md b/tests/Avalonia.IntegrationTests.Appium/readme.md new file mode 100644 index 00000000000..d824294fad3 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/readme.md @@ -0,0 +1,29 @@ +# Running Integration Tests + +## Windows + +### Prerequisites + +- Install WinAppDriver: https://github.com/microsoft/WinAppDriver + +### Running + +- Run WinAppDriver (it gets installed to the start menu) +- Run the tests in this project + +## MacOS + +### Prerequisites + +- Install Appium: https://appium.io/ +- `cd samples/IntegrationTestApp` then `./bundle.sh` to create an app bundle for `IntegrationTestApp` +- Register the app bundle by running `open -n ./bin/Debug/net6.0/osx-arm64/publish/IntegrationTestApp.app` + +### Running + +- Run `appium` +- Run the tests in this project + +Each time you make a change to Avalonia or `IntegrationTestApp`, re-run the `bundle.sh` script (registration only needs to be done once). + + From e89bd580d82d6e164d346d1e2bed4822804d1003 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 23 Nov 2021 18:54:48 +0100 Subject: [PATCH 63/73] Add additional macOS prerequisite. --- tests/Avalonia.IntegrationTests.Appium/readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Avalonia.IntegrationTests.Appium/readme.md b/tests/Avalonia.IntegrationTests.Appium/readme.md index d824294fad3..2a8c3068baa 100644 --- a/tests/Avalonia.IntegrationTests.Appium/readme.md +++ b/tests/Avalonia.IntegrationTests.Appium/readme.md @@ -16,6 +16,7 @@ ### Prerequisites - Install Appium: https://appium.io/ +- Give [XCode helper the required permissions](https://apple.stackexchange.com/questions/334008) - `cd samples/IntegrationTestApp` then `./bundle.sh` to create an app bundle for `IntegrationTestApp` - Register the app bundle by running `open -n ./bin/Debug/net6.0/osx-arm64/publish/IntegrationTestApp.app` From 7f8624e8a6f12309060471fc650689d11b23bbd9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 24 Nov 2021 10:48:34 +0100 Subject: [PATCH 64/73] Updated ApiCompat. --- src/Avalonia.Controls/ApiCompatBaseline.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 509e11b8573..dd41c30e852 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -50,9 +50,6 @@ MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.Resized.s InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public System.Func Avalonia.Platform.IWindowBaseImpl.AutomationStarted' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public System.Func Avalonia.Platform.IWindowBaseImpl.AutomationStarted.get()' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.AutomationStarted.set(System.Func)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean, System.Boolean)' is present in the implementation but not in the contract. @@ -60,4 +57,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract. -Total Issues: 61 +Total Issues: 58 From cc4cfa67ad9d07ef7c91e3a8239a52fc2463e97e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 30 Nov 2021 09:49:49 +0100 Subject: [PATCH 65/73] Throw when interface not supported. --- src/Windows/Avalonia.Win32/Automation/AutomationNode.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index f2ba8759454..70e415aff17 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; @@ -223,7 +222,6 @@ protected void InvokeSync(Action action) Dispatcher.UIThread.InvokeAsync(action).Wait(); } - [return: MaybeNull] protected T InvokeSync(Func func) { if (Dispatcher.UIThread.CheckAccess()) @@ -245,9 +243,12 @@ protected void InvokeSync(Action action) throw new COMException(e.Message, UiaCoreProviderApi.UIA_E_ELEMENTNOTENABLED); } } + else + { + throw new NotSupportedException(); + } } - [return: MaybeNull] protected TResult InvokeSync(Func func) { if (Peer.GetProvider() is TInterface i) @@ -262,7 +263,7 @@ protected TResult InvokeSync(Func func } } - return default; + throw new NotSupportedException(); } protected void RaiseFocusChanged(AutomationNode? focused) From 98a070e1af5e7788c9a04497327f98bfc6932555 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 30 Nov 2021 09:50:32 +0100 Subject: [PATCH 66/73] Implement provider members as explicit interface members. Previous change showed up a problem where incorrect interface was getting called for `IsReadOnly`. --- .../AutomationNode.ExpandCollapse.cs | 6 +++--- .../Automation/AutomationNode.RangeValue.cs | 10 +++++----- .../Automation/AutomationNode.Scroll.cs | 18 +++++++++--------- .../Automation/AutomationNode.Selection.cs | 16 ++++++++-------- .../Automation/AutomationNode.Toggle.cs | 4 ++-- .../Automation/AutomationNode.Value.cs | 5 +++-- 6 files changed, 30 insertions(+), 29 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs index 34ae969afe2..5f3f863493a 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs @@ -8,12 +8,12 @@ namespace Avalonia.Win32.Automation { internal partial class AutomationNode : UIA.IExpandCollapseProvider { - public ExpandCollapseState ExpandCollapseState + ExpandCollapseState UIA.IExpandCollapseProvider.ExpandCollapseState { get => InvokeSync(x => x.ExpandCollapseState); } - public void Expand() => InvokeSync(x => x.Expand()); - public void Collapse() => InvokeSync(x => x.Collapse()); + void UIA.IExpandCollapseProvider.Expand() => InvokeSync(x => x.Expand()); + void UIA.IExpandCollapseProvider.Collapse() => InvokeSync(x => x.Collapse()); } } diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs index d7e97cb30ca..b91cb76888c 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs @@ -8,11 +8,11 @@ namespace Avalonia.Win32.Automation internal partial class AutomationNode : UIA.IRangeValueProvider { double UIA.IRangeValueProvider.Value => InvokeSync(x => x.Value); - public bool IsReadOnly => InvokeSync(x => x.IsReadOnly); - public double Maximum => InvokeSync(x => x.Maximum); - public double Minimum => InvokeSync(x => x.Minimum); - public double LargeChange => 1; - public double SmallChange => 1; + bool UIA.IRangeValueProvider.IsReadOnly => InvokeSync(x => x.IsReadOnly); + double UIA.IRangeValueProvider.Maximum => InvokeSync(x => x.Maximum); + double UIA.IRangeValueProvider.Minimum => InvokeSync(x => x.Minimum); + double UIA.IRangeValueProvider.LargeChange => 1; + double UIA.IRangeValueProvider.SmallChange => 1; public void SetValue(double value) => InvokeSync(x => x.SetValue(value)); } diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs index 55f1aba71cc..4f2d4ae2691 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs @@ -7,24 +7,24 @@ namespace Avalonia.Win32.Automation { internal partial class AutomationNode : UIA.IScrollProvider, UIA.IScrollItemProvider { - public bool HorizontallyScrollable => InvokeSync(x => x.HorizontallyScrollable); - public double HorizontalScrollPercent => InvokeSync(x => x.HorizontalScrollPercent); - public double HorizontalViewSize => InvokeSync(x => x.HorizontalViewSize); - public bool VerticallyScrollable => InvokeSync(x => x.VerticallyScrollable); - public double VerticalScrollPercent => InvokeSync(x => x.VerticalScrollPercent); - public double VerticalViewSize => InvokeSync(x => x.VerticalViewSize); + bool UIA.IScrollProvider.HorizontallyScrollable => InvokeSync(x => x.HorizontallyScrollable); + double UIA.IScrollProvider.HorizontalScrollPercent => InvokeSync(x => x.HorizontalScrollPercent); + double UIA.IScrollProvider.HorizontalViewSize => InvokeSync(x => x.HorizontalViewSize); + bool UIA.IScrollProvider.VerticallyScrollable => InvokeSync(x => x.VerticallyScrollable); + double UIA.IScrollProvider.VerticalScrollPercent => InvokeSync(x => x.VerticalScrollPercent); + double UIA.IScrollProvider.VerticalViewSize => InvokeSync(x => x.VerticalViewSize); - public void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount) + void UIA.IScrollProvider.Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount) { InvokeSync(x => x.Scroll(horizontalAmount, verticalAmount)); } - public void SetScrollPercent(double horizontalPercent, double verticalPercent) + void UIA.IScrollProvider.SetScrollPercent(double horizontalPercent, double verticalPercent) { InvokeSync(x => x.SetScrollPercent(horizontalPercent, verticalPercent)); } - public void ScrollIntoView() + void UIA.IScrollItemProvider.ScrollIntoView() { InvokeSync(() => Peer.BringIntoView()); } diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs index c41ace4aa84..61903ab5b08 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs @@ -11,11 +11,11 @@ namespace Avalonia.Win32.Automation { internal partial class AutomationNode : UIA.ISelectionProvider, UIA.ISelectionItemProvider { - public bool CanSelectMultiple => InvokeSync(x => x.CanSelectMultiple); - public bool IsSelectionRequired => InvokeSync(x => x.IsSelectionRequired); - public bool IsSelected => InvokeSync(x => x.IsSelected); + bool UIA.ISelectionProvider.CanSelectMultiple => InvokeSync(x => x.CanSelectMultiple); + bool UIA.ISelectionProvider.IsSelectionRequired => InvokeSync(x => x.IsSelectionRequired); + bool UIA.ISelectionItemProvider.IsSelected => InvokeSync(x => x.IsSelected); - public UIA.IRawElementProviderSimple? SelectionContainer + UIA.IRawElementProviderSimple? UIA.ISelectionItemProvider.SelectionContainer { get { @@ -24,15 +24,15 @@ public UIA.IRawElementProviderSimple? SelectionContainer } } - public UIA.IRawElementProviderSimple[] GetSelection() + UIA.IRawElementProviderSimple[] UIA.ISelectionProvider.GetSelection() { var peers = InvokeSync>(x => x.GetSelection()); return peers?.Select(x => (UIA.IRawElementProviderSimple)GetOrCreate(x)!).ToArray() ?? Array.Empty(); } - public void AddToSelection() => InvokeSync(x => x.AddToSelection()); - public void RemoveFromSelection() => InvokeSync(x => x.RemoveFromSelection()); - public void Select() => InvokeSync(x => x.Select()); + void UIA.ISelectionItemProvider.AddToSelection() => InvokeSync(x => x.AddToSelection()); + void UIA.ISelectionItemProvider.RemoveFromSelection() => InvokeSync(x => x.RemoveFromSelection()); + void UIA.ISelectionItemProvider.Select() => InvokeSync(x => x.Select()); } } diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs index 90475557852..38f4d809461 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs @@ -7,7 +7,7 @@ namespace Avalonia.Win32.Automation { internal partial class AutomationNode : UIA.IToggleProvider { - public ToggleState ToggleState => InvokeSync(x => x.ToggleState); - public void Toggle() => InvokeSync(x => x.Toggle()); + ToggleState UIA.IToggleProvider.ToggleState => InvokeSync(x => x.ToggleState); + void UIA.IToggleProvider.Toggle() => InvokeSync(x => x.Toggle()); } } diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs index 06e67086631..34f5dfe0b92 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs @@ -8,9 +8,10 @@ namespace Avalonia.Win32.Automation { internal partial class AutomationNode : UIA.IValueProvider { - public string? Value => InvokeSync(x => x.Value); + bool UIA.IValueProvider.IsReadOnly => InvokeSync(x => x.IsReadOnly); + string? UIA.IValueProvider.Value => InvokeSync(x => x.Value); - public void SetValue([MarshalAs(UnmanagedType.LPWStr)] string? value) + void UIA.IValueProvider.SetValue([MarshalAs(UnmanagedType.LPWStr)] string? value) { InvokeSync(x => x.SetValue(value)); } From 1d665c55f96dc541b06e350b017484ef56aeb385 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 3 Dec 2021 16:02:03 +0100 Subject: [PATCH 67/73] Fix leak in GetNSStringAndRelease. --- native/Avalonia.Native/src/OSX/AvnString.mm | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index e0266a127c6..5e50068c516 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -156,17 +156,16 @@ virtual HRESULT Get(unsigned int index, IAvnString**ppv) override NSString* GetNSStringAndRelease(IAvnString* s) { + NSString* result = nil; + if (s != nullptr) { char* p; - if (s->Pointer((void**)&p) == S_OK && p != nullptr) - { - return [NSString stringWithUTF8String:p]; - } + result = [NSString stringWithUTF8String:p]; s->Release(); } - return nullptr; + return result; } From bff461679596182d07195b7179e1f57310d867d7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 5 Dec 2021 16:34:22 +0100 Subject: [PATCH 68/73] Don't run integration tests under ncrunch. --- .ncrunch/Avalonia.IntegrationTests.Appium.v3.ncrunchproject | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .ncrunch/Avalonia.IntegrationTests.Appium.v3.ncrunchproject diff --git a/.ncrunch/Avalonia.IntegrationTests.Appium.v3.ncrunchproject b/.ncrunch/Avalonia.IntegrationTests.Appium.v3.ncrunchproject new file mode 100644 index 00000000000..319cd523cec --- /dev/null +++ b/.ncrunch/Avalonia.IntegrationTests.Appium.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file From 09e326121eb0e9d87b5ec5c9f28de036f0f763a0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Jan 2022 15:44:17 +0100 Subject: [PATCH 69/73] Changed by VS. --- Avalonia.sln | 3 --- 1 file changed, 3 deletions(-) diff --git a/Avalonia.sln b/Avalonia.sln index f5286edab24..896e76cc81b 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -230,9 +230,6 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestApp", "samples\IntegrationTestApp\IntegrationTestApp.csproj", "{676D6BFD-029D-4E43-BFC7-3892265CE251}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.IntegrationTests.Appium", "tests\Avalonia.IntegrationTests.Appium\Avalonia.IntegrationTests.Appium.csproj", "{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}" -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.MicroCom", "src\Avalonia.MicroCom\Avalonia.MicroCom.csproj", "{FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniMvvm", "samples\MiniMvvm\MiniMvvm.csproj", "{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}" EndProject From 7581ce29efabb225616d28166262d202875f9e5e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 28 Jan 2022 22:47:14 +0100 Subject: [PATCH 70/73] Remove non-existent project. --- Avalonia.sln | 2 -- 1 file changed, 2 deletions(-) diff --git a/Avalonia.sln b/Avalonia.sln index 896e76cc81b..421dff32ba3 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -221,8 +221,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "samples\Sandbox\Sandbox.csproj", "{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroComGenerator", "src\tools\MicroComGenerator\MicroComGenerator.csproj", "{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.MicroCom", "src\Avalonia.MicroCom\Avalonia.MicroCom.csproj", "{FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniMvvm", "samples\MiniMvvm\MiniMvvm.csproj", "{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}" From 72beb30f025bf4b808a0bfd59d7ba603c883dc0f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 28 Jan 2022 23:04:08 +0100 Subject: [PATCH 71/73] Cast to interface instead of concrete type. `PlatformImpl` may be a `ValidatingTopLevelImpl`. --- src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs index 435e0671df6..8c7dfb5de20 100644 --- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -2,6 +2,7 @@ using System.Runtime.InteropServices; using Avalonia.Automation.Peers; using Avalonia.Automation.Provider; +using Avalonia.Platform; using Avalonia.Win32.Interop.Automation; #nullable enable @@ -21,7 +22,7 @@ public RootAutomationNode(AutomationPeer peer) public override IRawElementProviderFragmentRoot? FragmentRoot => this; public new IRootProvider Peer { get; } - public WindowImpl? WindowImpl => Peer.PlatformImpl as WindowImpl; + public IWindowImpl? WindowImpl => Peer.PlatformImpl as IWindowImpl; public IRawElementProviderFragment? ElementProviderFromPoint(double x, double y) { From d6b09a9f4b2ca4767ca89da712be319610161398 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 23 Feb 2022 23:08:27 +0100 Subject: [PATCH 72/73] Use correct interface for root window impl. --- src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs index 8c7dfb5de20..1085aa1b424 100644 --- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -22,8 +22,8 @@ public RootAutomationNode(AutomationPeer peer) public override IRawElementProviderFragmentRoot? FragmentRoot => this; public new IRootProvider Peer { get; } - public IWindowImpl? WindowImpl => Peer.PlatformImpl as IWindowImpl; - + public IWindowBaseImpl? WindowImpl => Peer.PlatformImpl as IWindowBaseImpl; + public IRawElementProviderFragment? ElementProviderFromPoint(double x, double y) { if (WindowImpl is null) From b9f9d4d4b9ef5454c1a3018cc7f0b1a61fe318ce Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 24 Feb 2022 14:17:13 +0100 Subject: [PATCH 73/73] Fix nullable errors after merge. And remove now-unneeded `#nullable enable` statements. --- .../AutomationPropertyChangedEventArgs.cs | 2 -- .../Automation/Peers/AutomationPeer.cs | 2 -- .../Automation/Peers/ButtonAutomationPeer.cs | 2 -- .../Automation/Peers/ComboBoxAutomationPeer.cs | 4 +--- .../Peers/ContentControlAutomationPeer.cs | 2 -- .../Automation/Peers/ControlAutomationPeer.cs | 6 ++---- .../Peers/ItemsControlAutomationPeer.cs | 2 -- .../Automation/Peers/ListItemAutomationPeer.cs | 2 -- .../Automation/Peers/MenuItemAutomationPeer.cs | 2 -- .../Automation/Peers/NoneAutomationPeer.cs | 2 -- .../Automation/Peers/PopupAutomationPeer.cs | 4 +--- .../Peers/PopupRootAutomationPeer.cs | 6 ++---- .../Peers/RangeBaseAutomationPeer.cs | 4 +--- .../Peers/ScrollViewerAutomationPeer.cs | 2 -- .../SelectingItemsControlAutomationPeer.cs | 6 ++---- .../Peers/TextBlockAutomationPeer.cs | 2 -- .../Automation/Peers/TextBoxAutomationPeer.cs | 2 -- .../Peers/ToggleButtonAutomationPeer.cs | 2 -- .../Peers/UnrealizedElementAutomationPeer.cs | 2 -- .../Automation/Peers/WindowAutomationPeer.cs | 8 +++----- .../Peers/WindowBaseAutomationPeer.cs | 18 ++++++++++-------- .../Automation/Provider/IRangeValueProvider.cs | 4 +--- .../Automation/Provider/IRootProvider.cs | 2 -- .../Provider/ISelectionItemProvider .cs | 4 +--- .../Automation/Provider/IValueProvider.cs | 4 +--- 25 files changed, 25 insertions(+), 71 deletions(-) diff --git a/src/Avalonia.Controls/Automation/AutomationPropertyChangedEventArgs.cs b/src/Avalonia.Controls/Automation/AutomationPropertyChangedEventArgs.cs index b8018613f80..3b7eb70fcb5 100644 --- a/src/Avalonia.Controls/Automation/AutomationPropertyChangedEventArgs.cs +++ b/src/Avalonia.Controls/Automation/AutomationPropertyChangedEventArgs.cs @@ -1,7 +1,5 @@ using System; -#nullable enable - namespace Avalonia.Automation { public class AutomationPropertyChangedEventArgs : EventArgs diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index 54b2fcc7fac..71421ac1362 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -#nullable enable - namespace Avalonia.Automation.Peers { public enum AutomationControlType diff --git a/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs index f5d6dce0399..4ac07717da7 100644 --- a/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs @@ -1,8 +1,6 @@ using Avalonia.Automation.Provider; using Avalonia.Controls; -#nullable enable - namespace Avalonia.Automation.Peers { public class ButtonAutomationPeer : ContentControlAutomationPeer, diff --git a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs index 01225279501..5ff291d9725 100644 --- a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs @@ -3,8 +3,6 @@ using Avalonia.Automation.Provider; using Avalonia.Controls; -#nullable enable - namespace Avalonia.Automation.Peers { public class ComboBoxAutomationPeer : SelectingItemsControlAutomationPeer, @@ -60,7 +58,7 @@ protected override AutomationControlType GetAutomationControlTypeCore() return null; } - protected override void OwnerPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + protected override void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { base.OwnerPropertyChanged(sender, e); diff --git a/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs index 08e4f2a9265..df24222a0c5 100644 --- a/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs @@ -1,7 +1,5 @@ using Avalonia.Controls; -#nullable enable - namespace Avalonia.Automation.Peers { public class ContentControlAutomationPeer : ControlAutomationPeer diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index f7a993e16b8..28cb3e34b25 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -4,8 +4,6 @@ using Avalonia.Controls; using Avalonia.VisualTree; -#nullable enable - namespace Avalonia.Automation.Peers { /// @@ -174,9 +172,9 @@ private void Initialize() visualChildren.CollectionChanged += VisualChildrenChanged; } - private void VisualChildrenChanged(object sender, EventArgs e) => InvalidateChildren(); + private void VisualChildrenChanged(object? sender, EventArgs e) => InvalidateChildren(); - private void OwnerPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + private void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property == Visual.IsVisibleProperty) { diff --git a/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs index 0d35920e193..db16bf0a538 100644 --- a/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs @@ -1,8 +1,6 @@ using Avalonia.Automation.Provider; using Avalonia.Controls; -#nullable enable - namespace Avalonia.Automation.Peers { public class ItemsControlAutomationPeer : ControlAutomationPeer, IScrollProvider diff --git a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs index 0621e81c1a1..ac23873e6aa 100644 --- a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs @@ -4,8 +4,6 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Selection; -#nullable enable - namespace Avalonia.Automation.Peers { public class ListItemAutomationPeer : ContentControlAutomationPeer, diff --git a/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs index 1994f004d6a..c98c5c9a22f 100644 --- a/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs @@ -1,8 +1,6 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; -#nullable enable - namespace Avalonia.Automation.Peers { public class MenuItemAutomationPeer : ControlAutomationPeer diff --git a/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs index a9958861796..0f92fed6f3a 100644 --- a/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs @@ -1,7 +1,5 @@ using Avalonia.Controls; -#nullable enable - namespace Avalonia.Automation.Peers { /// diff --git a/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs index 9c24f855f8b..25f6ca6e2d4 100644 --- a/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs @@ -4,8 +4,6 @@ using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; -#nullable enable - namespace Avalonia.Automation.Peers { public class PopupAutomationPeer : ControlAutomationPeer @@ -26,7 +24,7 @@ public PopupAutomationPeer(Popup owner) protected override bool IsContentElementCore() => false; protected override bool IsControlElementCore() => false; - private void PopupOpenedClosed(object sender, EventArgs e) + private void PopupOpenedClosed(object? sender, EventArgs e) { // This is golden. We're following WPF's automation peer API here where the // parent of a peer is set when another peer returns it as a child. We want to diff --git a/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs index fb717ef98be..cb65682c068 100644 --- a/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs @@ -1,8 +1,6 @@ using System; using Avalonia.Controls.Primitives; -#nullable enable - namespace Avalonia.Automation.Peers { public class PopupRootAutomationPeer : WindowBaseAutomationPeer @@ -27,13 +25,13 @@ public PopupRootAutomationPeer(PopupRoot owner) return parent; } - private void OnOpened(object sender, EventArgs e) + private void OnOpened(object? sender, EventArgs e) { ((PopupRoot)Owner).Opened -= OnOpened; StartTrackingFocus(); } - private void OnClosed(object sender, EventArgs e) + private void OnClosed(object? sender, EventArgs e) { ((PopupRoot)Owner).Closed -= OnClosed; StopTrackingFocus(); diff --git a/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs index 1bb487b1614..39398933fa4 100644 --- a/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs @@ -1,8 +1,6 @@ using Avalonia.Automation.Provider; using Avalonia.Controls.Primitives; -#nullable enable - namespace Avalonia.Automation.Peers { public abstract class RangeBaseAutomationPeer : ControlAutomationPeer, IRangeValueProvider @@ -23,7 +21,7 @@ public RangeBaseAutomationPeer(RangeBase owner) public void SetValue(double value) => Owner.Value = value; - protected virtual void OwnerPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + protected virtual void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property == RangeBase.MinimumProperty) RaisePropertyChangedEvent(RangeValuePatternIdentifiers.MinimumProperty, e.OldValue, e.NewValue); diff --git a/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs index c2474bb9b83..835ed1c4af3 100644 --- a/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs @@ -3,8 +3,6 @@ using Avalonia.Controls; using Avalonia.Utilities; -#nullable enable - namespace Avalonia.Automation.Peers { public class ScrollViewerAutomationPeer : ControlAutomationPeer, IScrollProvider diff --git a/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs index f372e3b7813..4626e30ff10 100644 --- a/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs @@ -6,8 +6,6 @@ using Avalonia.Controls.Selection; using Avalonia.VisualTree; -#nullable enable - namespace Avalonia.Automation.Peers { public abstract class SelectingItemsControlAutomationPeer : ItemsControlAutomationPeer, @@ -62,7 +60,7 @@ protected virtual SelectionMode GetSelectionModeCore() return (Owner as SelectingItemsControl)?.GetValue(ListBox.SelectionModeProperty) ?? SelectionMode.Single; } - protected virtual void OwnerPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + protected virtual void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property == ListBox.SelectionProperty) { @@ -73,7 +71,7 @@ protected virtual void OwnerPropertyChanged(object sender, AvaloniaPropertyChang } } - protected virtual void OwnerSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) + protected virtual void OwnerSelectionChanged(object? sender, SelectionModelSelectionChangedEventArgs e) { RaiseSelectionChanged(); } diff --git a/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs index 37eaf6f7c0f..8a89e38f620 100644 --- a/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs @@ -1,7 +1,5 @@ using Avalonia.Controls; -#nullable enable - namespace Avalonia.Automation.Peers { public class TextBlockAutomationPeer : ControlAutomationPeer diff --git a/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs index 33b2ba58b6a..9be17afa8c7 100644 --- a/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs @@ -1,8 +1,6 @@ using Avalonia.Automation.Provider; using Avalonia.Controls; -#nullable enable - namespace Avalonia.Automation.Peers { public class TextBoxAutomationPeer : ControlAutomationPeer, IValueProvider diff --git a/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs index dd0c506d29f..979d54f48e9 100644 --- a/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs @@ -1,8 +1,6 @@ using Avalonia.Automation.Provider; using Avalonia.Controls.Primitives; -#nullable enable - namespace Avalonia.Automation.Peers { public class ToggleButtonAutomationPeer : ContentControlAutomationPeer, IToggleProvider diff --git a/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs index b388f21a17a..56d5aa79aee 100644 --- a/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -#nullable enable - namespace Avalonia.Automation.Peers { /// diff --git a/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs index fbc6e9d4f4e..1162132d54e 100644 --- a/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs @@ -1,8 +1,6 @@ using System; using Avalonia.Controls; -#nullable enable - namespace Avalonia.Automation.Peers { public class WindowAutomationPeer : WindowBaseAutomationPeer @@ -19,15 +17,15 @@ public WindowAutomationPeer(Window owner) public new Window Owner => (Window)base.Owner; - protected override string GetNameCore() => Owner.Title; + protected override string? GetNameCore() => Owner.Title; - private void OnOpened(object sender, EventArgs e) + private void OnOpened(object? sender, EventArgs e) { Owner.Opened -= OnOpened; StartTrackingFocus(); } - private void OnClosed(object sender, EventArgs e) + private void OnClosed(object? sender, EventArgs e) { Owner.Closed -= OnClosed; StopTrackingFocus(); diff --git a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs index b24685929ad..30b56bbd960 100644 --- a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs @@ -6,8 +6,6 @@ using Avalonia.Platform; using Avalonia.VisualTree; -#nullable enable - namespace Avalonia.Automation.Peers { public class WindowBaseAutomationPeer : ControlAutomationPeer, IRootProvider @@ -20,7 +18,7 @@ public WindowBaseAutomationPeer(WindowBase owner) } public new WindowBase Owner => (WindowBase)base.Owner; - public ITopLevelImpl PlatformImpl => Owner.PlatformImpl; + public ITopLevelImpl? PlatformImpl => Owner.PlatformImpl; public event EventHandler? FocusChanged; @@ -39,13 +37,17 @@ protected override AutomationControlType GetAutomationControlTypeCore() protected void StartTrackingFocus() { - KeyboardDevice.Instance.PropertyChanged += KeyboardDevicePropertyChanged; - OnFocusChanged(KeyboardDevice.Instance.FocusedElement); + if (KeyboardDevice.Instance is not null) + { + KeyboardDevice.Instance.PropertyChanged += KeyboardDevicePropertyChanged; + OnFocusChanged(KeyboardDevice.Instance.FocusedElement); + } } protected void StopTrackingFocus() { - KeyboardDevice.Instance.PropertyChanged -= KeyboardDevicePropertyChanged; + if (KeyboardDevice.Instance is not null) + KeyboardDevice.Instance.PropertyChanged -= KeyboardDevicePropertyChanged; } private void OnFocusChanged(IInputElement? focus) @@ -63,11 +65,11 @@ private void OnFocusChanged(IInputElement? focus) } } - private void KeyboardDevicePropertyChanged(object sender, PropertyChangedEventArgs e) + private void KeyboardDevicePropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(KeyboardDevice.FocusedElement)) { - OnFocusChanged(KeyboardDevice.Instance.FocusedElement); + OnFocusChanged(KeyboardDevice.Instance!.FocusedElement); } } } diff --git a/src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs b/src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs index d494e068f73..43a877a21a5 100644 --- a/src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs +++ b/src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs @@ -1,6 +1,4 @@ -#nullable enable - -namespace Avalonia.Automation.Provider +namespace Avalonia.Automation.Provider { /// /// Exposes methods and properties to support access by a UI Automation client to controls diff --git a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs index 8ea53863eb8..ce380595596 100644 --- a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs +++ b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs @@ -2,8 +2,6 @@ using Avalonia.Automation.Peers; using Avalonia.Platform; -#nullable enable - namespace Avalonia.Automation.Provider { public interface IRootProvider diff --git a/src/Avalonia.Controls/Automation/Provider/ISelectionItemProvider .cs b/src/Avalonia.Controls/Automation/Provider/ISelectionItemProvider .cs index 767d6bd7a2d..6cea1d13500 100644 --- a/src/Avalonia.Controls/Automation/Provider/ISelectionItemProvider .cs +++ b/src/Avalonia.Controls/Automation/Provider/ISelectionItemProvider .cs @@ -1,6 +1,4 @@ -#nullable enable - -namespace Avalonia.Automation.Provider +namespace Avalonia.Automation.Provider { /// /// Exposes methods and properties to support access by a UI Automation client to individual, diff --git a/src/Avalonia.Controls/Automation/Provider/IValueProvider.cs b/src/Avalonia.Controls/Automation/Provider/IValueProvider.cs index 83dbde4ec26..e025e287823 100644 --- a/src/Avalonia.Controls/Automation/Provider/IValueProvider.cs +++ b/src/Avalonia.Controls/Automation/Provider/IValueProvider.cs @@ -1,6 +1,4 @@ -#nullable enable - -namespace Avalonia.Automation.Provider +namespace Avalonia.Automation.Provider { /// /// Exposes methods and properties to support access by a UI Automation client to controls