diff --git a/src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs b/src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs
new file mode 100644
index 0000000000..7f6bff11ad
--- /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 0000000000..55de657b32
--- /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 0000000000..8ff1210a76
--- /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 0000000000..16968b271d
--- /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 0000000000..ac73d50603
--- /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 0000000000..342bdaceb1
--- /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 0000000000..c6b4feeb50
--- /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 0000000000..128c1e1dcc
--- /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 0000000000..aefd1febd0
--- /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 0000000000..ded27f14bd
--- /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 0000000000..4e98cc7746
--- /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 0000000000..698bfd1460
--- /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 0000000000..db1c7e1aa7
--- /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 0000000000..7989986653
--- /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 0000000000..ad88941299
--- /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 0000000000..6eed22e6cf
--- /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 0000000000..a9e5089e43
--- /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 0000000000..38cd7a8d41
--- /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 0000000000..b1f7774fc9
--- /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 0000000000..894234e502
--- /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 0000000000..ed0e8f6d44
--- /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 0000000000..bafc4c14fc
--- /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 0000000000..165df6c032
--- /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 0000000000..b55f653a5d
--- /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 0000000000..172b9a1b54
--- /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 0000000000..d615be43f3
--- /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 0000000000..20021b5b96
--- /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 0000000000..c523feb0f8
--- /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 0000000000..8ee1aacb52
--- /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 0000000000..b103df3165
--- /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 0000000000..5832b04dd7
--- /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 0000000000..41778457e8
--- /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 0000000000..ac235b3898
--- /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 0000000000..cae3504a4a
--- /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 0000000000..da16611b9e
--- /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 0000000000..8346a908c2
--- /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 0000000000..f0308c226b
--- /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 0000000000..47d7211c92
--- /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 0000000000..d4cd35fcf9
--- /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 0000000000..698b4f26b9
--- /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 0000000000..1055a2f1e1
--- /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 0000000000..767d6bd7a2
--- /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 0000000000..bf21c0151f
--- /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 0000000000..67913e3204
--- /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 0000000000..83dbde4ec2
--- /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 0000000000..e3fa744cc0
--- /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 0000000000..ad10b96e17
--- /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 0000000000..ce4fdda739
--- /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 c779e4b0cb..c8720f7cee 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 @@ namespace Avalonia.Controls
}
}
+ 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 @@ namespace Avalonia.Controls
}
}
+ internal void PerformClick() => OnClick();
+
///
/// Called when the property changes.
///
diff --git a/src/Avalonia.Controls/CheckBox.cs b/src/Avalonia.Controls/CheckBox.cs
index 05d49a44b1..374ab33338 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 c5af5ffa7a..83b97cc3d6 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 @@ namespace Avalonia.Controls
_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 4aab92c428..6b63ac14cc 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 @@ namespace Avalonia.Controls
private DataTemplates? _dataTemplates;
private IControl? _focusAdorner;
+ private AutomationPeer? _automationPeer;
///
/// Gets or sets the control's focus adorner.
@@ -188,5 +191,23 @@ namespace Avalonia.Controls
_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 5fc7d8b6b6..35bb8316f6 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 @@ namespace Avalonia.Controls
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 4dc8aec6f3..b6c2e8656d 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -3,6 +3,8 @@ using System.Collections;
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 @@ namespace Avalonia.Controls
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 4fe5f4de40..5599a89b62 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 @@ namespace Avalonia.Controls
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 4da044fec1..5260561393 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 @@ namespace Avalonia.Controls
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 94099a970e..71bc08890e 100644
--- a/src/Avalonia.Controls/MenuItem.cs
+++ b/src/Avalonia.Controls/MenuItem.cs
@@ -3,6 +3,8 @@ using System.Collections.Generic;
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 @@ namespace Avalonia.Controls
}
}
+ 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 b445de0472..1837b90c5d 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 @@ namespace Avalonia.Controls.Primitives
{
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 da7352b77f..73dd68e7cb 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 @@ namespace Avalonia.Controls.Primitives
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 6b2c566422..434d34928f 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 @@ namespace Avalonia.Controls.Primitives
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 6b75149d62..fe6e03e7a4 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 @@ namespace Avalonia.Controls
_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 e02efc2bd2..a3d18d73a1 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 @@ namespace Avalonia.Controls
_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 306a9d3e6a..63c68b783c 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 @@ namespace Avalonia.Controls
}
}
}
+
+ 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 593643a1eb..4d50ef961d 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 @@ namespace Avalonia.Controls
}
}
}
+
+ 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 31517ba59d..eb23878c3d 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 @@ namespace Avalonia.Controls
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 54d3af9b59..e91d3dc44d 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -14,6 +14,8 @@ using Avalonia.Data;
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 @@ namespace Avalonia.Controls
}
}
+ 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 98f4cadc13..7648162493 100644
--- a/src/Avalonia.Controls/Window.cs
+++ b/src/Avalonia.Controls/Window.cs
@@ -4,6 +4,8 @@ using System.ComponentModel;
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 @@ namespace Avalonia.Controls
}
}
}
+
+ 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 5899824c29..79cda87001 100644
--- a/src/Avalonia.Input/KeyboardDevice.cs
+++ b/src/Avalonia.Input/KeyboardDevice.cs
@@ -24,30 +24,7 @@ namespace Avalonia.Input
// 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 @@ namespace Avalonia.Input
}
SetIsFocusWithin(FocusedElement, element);
-
- FocusedElement = element;
+ _focusedElement = element;
+ _focusedRoot = _focusedElement?.VisualRoot as IInputRoot;
interactive?.RaiseEvent(new RoutedEventArgs
{
@@ -178,6 +155,9 @@ namespace Avalonia.Input
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 0000000000..34ae969afe
--- /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 0000000000..d7e97cb30c
--- /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 0000000000..55f1aba71c
--- /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 0000000000..17634ae805
--- /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 0000000000..9047555785
--- /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 0000000000..06e6708663
--- /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 0000000000..ca237567c1
--- /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 0000000000..776b65adc6
--- /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 0000000000..cb8cfae90e
--- /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 fe5f806fbe..94d7ed6651 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 0000000000..2787434d26
--- /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 0000000000..67be1e6c71
--- /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 0000000000..f911c38472
--- /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 0000000000..a8caf26524
--- /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 0000000000..f35646d456
--- /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 0000000000..c487a0f5df
--- /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 0000000000..558f38a2cc
--- /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 0000000000..1e799e05a2
--- /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 0000000000..a62aa842cb
--- /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 0000000000..71d1bdce60
--- /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 0000000000..439036290e
--- /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 0000000000..f3504b8d77
--- /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 0000000000..c34c8667ef
--- /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 0000000000..154d52c6af
--- /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 0000000000..1de0cf0f9b
--- /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 0000000000..8a5924126d
--- /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 0000000000..def1bbd4b9
--- /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 0000000000..36751122d1
--- /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 0000000000..e82bda3272
--- /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 0000000000..3f8fbc80c7
--- /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 0000000000..9ebb4c9f49
--- /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 0000000000..e4072a1250
--- /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 0000000000..4859f2d078
--- /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 0000000000..919be647f8
--- /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 0000000000..d915beb601
--- /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 0000000000..4ba7a710d4
--- /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 0000000000..4375b2fde1
--- /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 e7d16f731c..b0d41b72eb 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 @@ namespace Avalonia.Win32
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 @@ namespace Avalonia.Win32
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 1ddec2e763..59927fd488 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -3,6 +3,8 @@ using System.Collections.Generic;
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.Egl;
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 @@ namespace Avalonia.Win32
private POINT _maxTrackSize;
private WindowImpl _parent;
private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default;
+ private AutomationNode _automationProvider;
private bool _isCloseRequested;
public WindowImpl()
@@ -727,7 +732,7 @@ namespace Avalonia.Win32
throw new Win32Exception();
}
- Handle = new PlatformHandle(_hwnd, PlatformConstants.WindowHandleType);
+ Handle = new WindowImplPlatformHandle(this);
_multitouch = Win32Platform.Options.EnableMultitouch ?? true;
@@ -1250,6 +1255,17 @@ namespace Avalonia.Win32
///
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 @@ namespace Avalonia.Win32
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 0000000000..250cad4e25
--- /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 9223b08c81..697179af74 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
@@ -491,6 +491,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
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 df0a077c7f..382bd762d5 100644
--- a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs
+++ b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs
@@ -86,5 +86,30 @@ namespace Avalonia.Input.UnitTests
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 8a24a8366f..983f48e5f0 100644
--- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
+++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
@@ -85,7 +85,12 @@ namespace Avalonia.UnitTests
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;