A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

390 lines
17 KiB

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Automation.Peers;
using Avalonia.Threading;
using Avalonia.Win32.Automation.Interop;
using AAP = Avalonia.Automation.Provider;
using UIA = Avalonia.Win32.Automation.Interop;
namespace Avalonia.Win32.Automation
{
#if NET8_0_OR_GREATER
[GeneratedComClass]
internal partial class AutomationNode :
#else
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Requires .NET COM interop")]
#endif
internal partial class AutomationNode : MarshalByRefObject,
#endif
IRawElementProviderSimple,
IRawElementProviderSimple2,
IRawElementProviderFragment,
IInvokeProvider
{
private static Dictionary<AutomationProperty, UiaPropertyId> s_propertyMap = new()
{
{ AutomationElementIdentifiers.BoundingRectangleProperty, UiaPropertyId.BoundingRectangle },
{ AutomationElementIdentifiers.ClassNameProperty, UiaPropertyId.ClassName },
{ AutomationElementIdentifiers.NameProperty, UiaPropertyId.Name },
{ AutomationElementIdentifiers.HelpTextProperty, UiaPropertyId.HelpText },
{ AutomationElementIdentifiers.HeadingLevelProperty, UiaPropertyId.HeadingLevel },
{ 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 },
{ SelectionItemPatternIdentifiers.IsSelectedProperty, UiaPropertyId.SelectionItemIsSelected },
{
SelectionItemPatternIdentifiers.SelectionContainerProperty,
UiaPropertyId.SelectionItemSelectionContainer
}
};
private static ConditionalWeakTable<AutomationPeer, AutomationNode> s_nodes = new();
private readonly int[] _runtimeId;
private static readonly int s_pid = GetProcessId();
public AutomationNode(AutomationPeer peer)
{
_runtimeId = new int[] { 3, GetHashCode() };
Peer = peer;
s_nodes.Add(peer, this);
peer.ChildrenChanged += OnPeerChildrenChanged;
peer.PropertyChanged += OnPeerPropertyChanged;
if (Peer.GetProvider<AAP.IEmbeddedRootProvider>() is { } embeddedRoot)
embeddedRoot.FocusChanged += OnEmbeddedRootFocusChanged;
}
public AutomationPeer Peer { get; protected set; }
public virtual Rect GetBoundingRectangle()
{
return InvokeSync(() =>
{
if (GetRoot() is RootAutomationNode root)
return root.ToScreen(Peer.GetBoundingRectangle());
return default;
});
}
public virtual IRawElementProviderFragmentRoot? GetFragmentRoot()
{
return InvokeSync(() => GetRoot());
}
public virtual IRawElementProviderSimple? GetHostRawElementProvider() => null;
public virtual ProviderOptions GetProviderOptions() => ProviderOptions.ServerSideProvider;
public virtual object? GetPatternProvider(int patternId)
{
AutomationNode? ThisIfPeerImplementsProvider<T>() => Peer.GetProvider<T>() is object ? this : null;
return (UiaPatternId)patternId switch
{
UiaPatternId.ExpandCollapse => ThisIfPeerImplementsProvider<AAP.IExpandCollapseProvider>(),
UiaPatternId.Invoke => ThisIfPeerImplementsProvider<AAP.IInvokeProvider>(),
UiaPatternId.RangeValue => ThisIfPeerImplementsProvider<AAP.IRangeValueProvider>(),
UiaPatternId.Scroll => ThisIfPeerImplementsProvider<AAP.IScrollProvider>(),
UiaPatternId.ScrollItem => this,
UiaPatternId.Selection => ThisIfPeerImplementsProvider<AAP.ISelectionProvider>(),
UiaPatternId.SelectionItem => ThisIfPeerImplementsProvider<AAP.ISelectionItemProvider>(),
UiaPatternId.Toggle => ThisIfPeerImplementsProvider<AAP.IToggleProvider>(),
UiaPatternId.Value => ThisIfPeerImplementsProvider<AAP.IValueProvider>(),
_ => null,
};
}
public virtual object? GetPropertyValue(int propertyId)
{
object? value = (UiaPropertyId)propertyId switch
{
UiaPropertyId.AcceleratorKey => InvokeSync(() => Peer.GetAcceleratorKey()),
UiaPropertyId.AccessKey => InvokeSync(() => Peer.GetAccessKey()),
UiaPropertyId.AutomationId => InvokeSync(() => Peer.GetAutomationId()),
UiaPropertyId.ClassName => InvokeSync(() => Peer.GetClassName()),
UiaPropertyId.ClickablePoint => GetBoundingRectangle() is var rect ?
new[] { rect.Center.X, rect.Center.Y } :
default,
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.IsOffscreen => InvokeSync(() => Peer.IsOffscreen()),
UiaPropertyId.LocalizedControlType => InvokeSync(() => Peer.GetLocalizedControlType()),
UiaPropertyId.Name => InvokeSync(() => Peer.GetName()),
UiaPropertyId.HelpText => InvokeSync(() => Peer.GetHelpText()),
UiaPropertyId.HeadingLevel => InvokeSync(() => ToUiaHeadingLevel(Peer.GetHeadingLevel())),
UiaPropertyId.ProcessId => s_pid,
UiaPropertyId.RuntimeId => _runtimeId,
_ => null,
};
if (value?.GetType().IsEnum == true)
{
return Convert.ToInt32(value!);
}
return value;
}
public int[] GetRuntimeId() => _runtimeId;
public virtual IRawElementProviderFragment? Navigate(NavigateDirection direction)
{
AutomationNode? 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 GetOrCreate(children[j]);
}
}
return null;
}
return InvokeSync(() =>
{
return direction switch
{
NavigateDirection.Parent => GetOrCreate(Peer.GetParent()),
NavigateDirection.NextSibling => GetSibling(1),
NavigateDirection.PreviousSibling => GetSibling(-1),
NavigateDirection.FirstChild => GetOrCreate(Peer.GetChildren().FirstOrDefault()),
NavigateDirection.LastChild => GetOrCreate(Peer.GetChildren().LastOrDefault()),
_ => null,
};
});
}
public void SetFocus() => InvokeSync(() => Peer.SetFocus());
#if NET6_0_OR_GREATER
[return: NotNullIfNotNull(nameof(peer))]
#endif
public static AutomationNode? GetOrCreate(AutomationPeer? peer)
{
return peer is null ? null : s_nodes.GetValue(peer, Create);
}
public static void Release(AutomationPeer peer) => s_nodes.Remove(peer);
IRawElementProviderSimple[]? IRawElementProviderFragment.GetEmbeddedFragmentRoots() => null;
void IRawElementProviderSimple2.ShowContextMenu() => InvokeSync(() => Peer.ShowContextMenu());
void IInvokeProvider.Invoke() => InvokeSync((AAP.IInvokeProvider x) => x.Invoke());
protected void InvokeSync(Action action)
{
if (Dispatcher.UIThread.CheckAccess())
action();
else
Dispatcher.UIThread.InvokeAsync(action).Wait();
}
protected T InvokeSync<T>(Func<T> func)
{
if (Dispatcher.UIThread.CheckAccess())
return func();
else
return Dispatcher.UIThread.InvokeAsync(func).Result;
}
protected void InvokeSync<TInterface>(Action<TInterface> action)
{
if (Peer.GetProvider<TInterface>() is TInterface i)
{
try
{
InvokeSync(() => action(i));
}
catch (AggregateException e) when (e.InnerException is ElementNotEnabledException)
{
throw new COMException(e.Message, UiaCoreProviderApi.UIA_E_ELEMENTNOTENABLED);
}
}
else
{
throw new NotSupportedException();
}
}
protected TResult InvokeSync<TInterface, TResult>(Func<TInterface, TResult> func)
{
if (Peer.GetProvider<TInterface>() 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);
}
}
throw new NotSupportedException();
}
protected void RaiseChildrenChanged()
{
UiaCoreProviderApi.UiaRaiseStructureChangedEvent(
this,
StructureChangeType.ChildrenInvalidated,
null,
0);
}
protected void RaiseFocusChanged(AutomationNode? focused)
{
UiaCoreProviderApi.UiaRaiseAutomationEvent(
focused,
(int)UiaEventId.AutomationFocusChanged);
}
private RootAutomationNode? GetRoot()
{
Dispatcher.UIThread.VerifyAccess();
return GetOrCreate(Peer.GetVisualRoot()) as RootAutomationNode;
}
private void OnPeerChildrenChanged(object? sender, EventArgs e)
{
RaiseChildrenChanged();
}
private void OnPeerPropertyChanged(object? sender, AutomationPropertyChangedEventArgs e)
{
if (s_propertyMap.TryGetValue(e.Property, out var id))
{
UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent(
this,
(int)id,
e.OldValue as IConvertible,
e.NewValue as IConvertible);
}
}
private void OnEmbeddedRootFocusChanged(object? sender, EventArgs e)
{
if (Peer.GetProvider<AAP.IEmbeddedRootProvider>() is { } embeddedRoot)
RaiseFocusChanged(GetOrCreate(embeddedRoot.GetFocus()));
}
private static AutomationNode Create(AutomationPeer peer)
{
if (peer is InteropAutomationPeer interop)
return new InteropAutomationNode(interop);
else if (peer.GetProvider<AAP.IRootProvider>() is not null)
return new RootAutomationNode(peer);
else
return new AutomationNode(peer);
}
private static UiaControlTypeId ToUiaControlType(AutomationControlType role)
{
return role switch
{
AutomationControlType.None => UiaControlTypeId.Group,
AutomationControlType.Button => UiaControlTypeId.Button,
AutomationControlType.Calendar => UiaControlTypeId.Calendar,
AutomationControlType.CheckBox => UiaControlTypeId.CheckBox,
AutomationControlType.ComboBox => UiaControlTypeId.ComboBox,
AutomationControlType.ComboBoxItem => UiaControlTypeId.ListItem,
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,
};
}
private static UiaHeadingLevel ToUiaHeadingLevel(int level)
{
return level switch
{
1 => UiaHeadingLevel.Level1,
2 => UiaHeadingLevel.Level2,
3 => UiaHeadingLevel.Level3,
4 => UiaHeadingLevel.Level4,
5 => UiaHeadingLevel.Level5,
6 => UiaHeadingLevel.Level6,
7 => UiaHeadingLevel.Level7,
8 => UiaHeadingLevel.Level8,
9 => UiaHeadingLevel.Level9,
_ => UiaHeadingLevel.None,
};
}
private static int GetProcessId()
{
#if NET6_0_OR_GREATER
return Environment.ProcessId;
#else
using var proccess = Process.GetCurrentProcess();
return proccess.Id;
#endif
}
}
}