From 2a44d8b56463c71e4a1757d43e8fa9b8e762ab98 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 9 Mar 2021 13:25:50 +0100 Subject: [PATCH] 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;