diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
index 3d3fe35d29..6421557152 100644
--- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
@@ -95,6 +95,12 @@ namespace Avalonia.Automation.Peers
///
public string GetClassName() => GetClassNameCore() ?? string.Empty;
+ ///
+ /// Gets text that describes the functionality of the control that is associated with the
+ /// automation peer.
+ ///
+ public string? GetHelpText() => GetHelpTextCore();
+
///
/// Gets the automation peer for the label that is targeted to the element.
///
@@ -231,6 +237,7 @@ namespace Avalonia.Automation.Peers
protected abstract Rect GetBoundingRectangleCore();
protected abstract IReadOnlyList GetOrCreateChildrenCore();
protected abstract string GetClassNameCore();
+ protected abstract string? GetHelpTextCore();
protected abstract AutomationPeer? GetLabeledByCore();
protected abstract string? GetNameCore();
protected abstract AutomationPeer? GetParentCore();
diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
index c55bd0f3e5..b7adffdf93 100644
--- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
@@ -148,6 +148,7 @@ namespace Avalonia.Automation.Peers
protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner) ?? Owner.Name;
protected override Rect GetBoundingRectangleCore() => GetBounds(Owner);
protected override string GetClassNameCore() => Owner.GetType().Name;
+ protected override string? GetHelpTextCore() => AutomationProperties.GetHelpText(Owner);
protected override bool HasKeyboardFocusCore() => Owner.IsFocused;
protected override bool IsContentElementCore() => true;
protected override bool IsControlElementCore() => true;
diff --git a/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs
index 8a89e38f62..06b20bf7a2 100644
--- a/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs
@@ -1,21 +1,86 @@
-using Avalonia.Controls;
+using System;
+using System.Collections.Generic;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+using Avalonia.Reactive;
+using Avalonia.Utilities;
+using Avalonia.VisualTree;
namespace Avalonia.Automation.Peers
{
- public class TextBlockAutomationPeer : ControlAutomationPeer
+ public class TextBlockAutomationPeer : ControlAutomationPeer, ITextProvider
{
public TextBlockAutomationPeer(TextBlock owner)
: base(owner)
{
+ owner.GetObservable(TextBox.TextProperty).Subscribe(OnTextChanged);
}
+ public int CaretIndex => -1;
public new TextBlock Owner => (TextBlock)base.Owner;
+ public TextRange DocumentRange => new TextRange(0, Owner.Text?.Length ?? 0);
+ public bool IsReadOnly => true;
+ public int LineCount => Owner.TextLayout.TextLines.Count;
+ public string? PlaceholderText => null;
+ public SupportedTextSelection SupportedTextSelection => SupportedTextSelection.None;
- protected override AutomationControlType GetAutomationControlTypeCore()
+ public event EventHandler? TextChanged;
+ event EventHandler? ITextProvider.SelectedRangesChanged { add { } remove { } }
+
+ public IReadOnlyList GetBounds(TextRange range)
+ {
+ if (Owner.GetVisualRoot() is Visual root &&
+ Owner.TransformToVisual(root) is Matrix m)
+ {
+ var source = Owner.TextLayout.HitTestTextRange(range.Start, range.Length);
+ var result = new List();
+ foreach (var rect in source)
+ result.Add(rect.TransformToAABB(m));
+ return result;
+ }
+
+ return Array.Empty();
+ }
+
+ public int GetLineForIndex(int index)
+ {
+ return Owner.TextLayout.GetLineIndexFromCharacterIndex(index, false);
+ }
+
+ public TextRange GetLineRange(int lineIndex)
+ {
+ var line = Owner.TextLayout.TextLines[lineIndex];
+ return new TextRange(line.FirstTextSourceIndex, line.Length);
+ }
+
+ public string GetText(TextRange range)
+ {
+ var text = Owner.Text ?? string.Empty;
+ var start = MathUtilities.Clamp(range.Start, 0, text.Length);
+ var end = MathUtilities.Clamp(range.End, 0, text.Length);
+ return text.Substring(start, end - start);
+ }
+
+ public IReadOnlyList GetVisibleRanges() => new[] { DocumentRange };
+
+ public TextRange RangeFromPoint(Point p)
{
- return AutomationControlType.Text;
+ var i = 0;
+
+ if (Owner.GetVisualRoot() is Visual root &&
+ root.TransformToVisual(Owner) is Matrix m)
+ {
+ i = Owner.TextLayout.HitTestPoint(p.Transform(m)).TextPosition;
+ }
+
+ return new TextRange(i, 1);
}
+ IReadOnlyList ITextProvider.GetSelection() => Array.Empty();
+ void ITextProvider.ScrollIntoView(TextRange range) { }
+ void ITextProvider.Select(TextRange range) { }
+
+ protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Text;
protected override string? GetNameCore() => Owner.Text;
protected override bool IsControlElementCore()
@@ -23,5 +88,8 @@ namespace Avalonia.Automation.Peers
// Return false if the control is part of a control template.
return Owner.TemplatedParent is null && base.IsControlElementCore();
}
+
+ private void OnTextChanged(string? text) => TextChanged?.Invoke(this, EventArgs.Empty);
+
}
}
diff --git a/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs
index 9be17afa8c..44daa0643b 100644
--- a/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs
@@ -1,23 +1,131 @@
-using Avalonia.Automation.Provider;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Avalonia.Automation.Provider;
using Avalonia.Controls;
+using Avalonia.Reactive;
+using Avalonia.Utilities;
+using Avalonia.VisualTree;
namespace Avalonia.Automation.Peers
{
- public class TextBoxAutomationPeer : ControlAutomationPeer, IValueProvider
+ public class TextBoxAutomationPeer : ControlAutomationPeer, ITextProvider, IValueProvider
{
public TextBoxAutomationPeer(TextBox owner)
: base(owner)
{
+ owner.GetObservable(TextBox.SelectionStartProperty).Subscribe(OnSelectionChanged);
+ owner.GetObservable(TextBox.SelectionEndProperty).Subscribe(OnSelectionChanged);
+ owner.GetObservable(TextBox.TextProperty).Subscribe(OnTextChanged);
}
+ public int CaretIndex => Owner.CaretIndex;
public new TextBox Owner => (TextBox)base.Owner;
public bool IsReadOnly => Owner.IsReadOnly;
+ public int LineCount => Owner.Presenter?.TextLayout.TextLines.Count ?? 0;
public string? Value => Owner.Text;
+ public TextRange DocumentRange => new TextRange(0, Owner.Text?.Length ?? 0);
+ public string? PlaceholderText => Owner.Watermark;
+ public SupportedTextSelection SupportedTextSelection => SupportedTextSelection.Single;
+
+ public event EventHandler? SelectedRangesChanged;
+ public event EventHandler? TextChanged;
+
+ public IReadOnlyList GetBounds(TextRange range)
+ {
+ if (Owner.GetVisualRoot() is Visual root &&
+ Owner.Presenter?.TransformToVisual(root) is Matrix m)
+ {
+ var source = Owner.Presenter?.TextLayout.HitTestTextRange(range.Start, range.Length)
+ ?? Array.Empty();
+ var result = new List();
+ foreach (var rect in source)
+ result.Add(rect.TransformToAABB(m));
+ return result;
+ }
+
+ return Array.Empty();
+ }
+
+ public int GetLineForIndex(int index)
+ {
+ return Owner.Presenter?.TextLayout.GetLineIndexFromCharacterIndex(index, false) ?? -1;
+ }
+
+ public TextRange GetLineRange(int lineIndex)
+ {
+ if (Owner.Presenter is null)
+ return TextRange.Empty;
+
+ var line = Owner.Presenter.TextLayout.TextLines[lineIndex];
+ return new TextRange(line.FirstTextSourceIndex, line.Length);
+ }
+
+ public IReadOnlyList GetSelection()
+ {
+ var range = TextRange.FromInclusiveStartEnd(Owner.SelectionStart, Owner.SelectionEnd);
+ return new[] { range };
+ }
+
+ public string GetText(TextRange range)
+ {
+ var text = Owner.Text ?? string.Empty;
+ var start = MathUtilities.Clamp(range.Start, 0, text.Length);
+ var end = MathUtilities.Clamp(range.End, 0, text.Length);
+ return text.Substring(start, end - start);
+ }
+
+ public IReadOnlyList GetVisibleRanges()
+ {
+ // Not sure this is necessary, QT just returns the document range too.
+ return new[] { DocumentRange };
+ }
+
+ public TextRange RangeFromPoint(Point p)
+ {
+ if (Owner.Presenter is null)
+ return TextRange.Empty;
+
+ var i = 0;
+
+ if (Owner.GetVisualRoot() is Visual root &&
+ root.TransformToVisual(Owner) is Matrix m)
+ {
+ i = Owner.Presenter.TextLayout.HitTestPoint(p.Transform(m)).TextPosition;
+ }
+
+ return new TextRange(i, 1);
+ }
+
+ public void ScrollIntoView(TextRange range)
+ {
+ if (Owner.Presenter is null || Owner.Scroll is null)
+ return;
+
+ var rects = Owner.Presenter.TextLayout.HitTestTextRange(range.Start, range.Length);
+ var rect = default(Rect);
+
+ foreach (var r in rects)
+ rect = rect.Union(r);
+
+ Owner.Presenter.BringIntoView(rect);
+ }
+
+ public void Select(TextRange range)
+ {
+ Owner.SelectionStart = range.Start;
+ Owner.SelectionEnd = range.End;
+ }
+
public void SetValue(string? value) => Owner.Text = value;
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.Edit;
}
+
+
+ private void OnSelectionChanged(int obj) => SelectedRangesChanged?.Invoke(this, EventArgs.Empty);
+ private void OnTextChanged(string? text) => TextChanged?.Invoke(this, EventArgs.Empty);
}
}
diff --git a/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs
index 56d5aa79ae..1a468ca78d 100644
--- a/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs
@@ -11,6 +11,7 @@ namespace Avalonia.Automation.Peers
public void SetParent(AutomationPeer? parent) => TrySetParent(parent);
protected override void BringIntoViewCore() => GetParent()?.BringIntoView();
protected override Rect GetBoundingRectangleCore() => GetParent()?.GetBoundingRectangle() ?? default;
+ protected override string? GetHelpTextCore() => null;
protected override IReadOnlyList GetOrCreateChildrenCore() => Array.Empty();
protected override bool HasKeyboardFocusCore() => false;
protected override bool IsContentElementCore() => false;
diff --git a/src/Avalonia.Controls/Automation/Provider/ITextProvider.cs b/src/Avalonia.Controls/Automation/Provider/ITextProvider.cs
new file mode 100644
index 0000000000..39b7fa0970
--- /dev/null
+++ b/src/Avalonia.Controls/Automation/Provider/ITextProvider.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Automation.Peers;
+
+#nullable enable
+
+namespace Avalonia.Automation.Provider
+{
+ ///
+ /// Describes the type of text selection supported by an element.
+ ///
+ public enum SupportedTextSelection
+ {
+ ///
+ /// The element does not support text selection.
+ ///
+ None,
+
+ ///
+ /// The element supports a single, continuous text selection.
+ ///
+ Single,
+
+ ///
+ /// The element supports multiple, disjoint text selections.
+ ///
+ Multiple,
+ }
+
+ ///
+ /// Exposes methods and properties to support access by a UI Automation client to controls
+ /// that holds editable text.
+ ///
+ public interface ITextProvider
+ {
+ ///
+ /// Gets the current position of the caret, or -1 if there is no caret.
+ ///
+ int CaretIndex { get; }
+
+ ///
+ /// Gets a text range that encloses the main text of the document.
+ ///
+ TextRange DocumentRange { get; }
+
+ ///
+ /// Gets a value that indicates whether the text in the control is read-only.
+ ///
+ bool IsReadOnly { get; }
+
+ ///
+ /// Gets the total number of lines in the main text of the document.
+ ///
+ int LineCount { get; }
+
+ ///
+ /// Gets the placeholder text.
+ ///
+ string? PlaceholderText { get; }
+
+ ///
+ /// Gets a value that specifies the type of text selection that is supported by the control.
+ ///
+ SupportedTextSelection SupportedTextSelection { get; }
+
+ ///
+ /// Occurs when the control's selection changes.
+ ///
+ event EventHandler? SelectedRangesChanged;
+
+ ///
+ /// Occurs when the control's text changes.
+ ///
+ event EventHandler? TextChanged;
+
+ ///
+ /// Retrieves a collection of bounding rectangles for each fully or partially visible line
+ /// of text in a text range.
+ ///
+ /// The text range.
+ /// A collection of s in top-level coordinates.
+ IReadOnlyList GetBounds(TextRange range);
+
+ ///
+ /// Retrieves the line number for the line that contains the specified character index.
+ ///
+ /// The character index.
+ /// The line number, or -1 if the character index is invalid.
+ int GetLineForIndex(int index);
+
+ ///
+ /// Retrieves a text range that encloses the specified line.
+ ///
+ /// The index of the line.
+ TextRange GetLineRange(int lineIndex);
+
+ ///
+ /// Retrieves a collection of text ranges that represents the currently selected text in a
+ /// text-based control.
+ ///
+ ///
+ /// If the control contains a text insertion point but no text is selected, the result
+ /// should contain a degenerate (empty) text range at the position of the text insertion
+ /// point.
+ ///
+ IReadOnlyList GetSelection();
+
+ ///
+ /// Retrieves the text for a specified range.
+ ///
+ /// The text range.
+ /// The text.
+ string GetText(TextRange range);
+
+ ///
+ /// Retrieves a collection of disjoint text ranges from a text-based control where each
+ /// text range represents a contiguous span of visible text.
+ ///
+ IReadOnlyList GetVisibleRanges();
+
+ ///
+ /// Returns the degenerate (empty) text range nearest to the specified coordinates.
+ ///
+ /// The point in top-level coordinates.
+ TextRange RangeFromPoint(Point p);
+
+ ///
+ /// Scrolls the specified range of text into view.
+ ///
+ /// The text range.
+ void ScrollIntoView(TextRange range);
+
+ ///
+ /// Selects the specified range of text, replacing any previous selection.
+ ///
+ ///
+ void Select(TextRange range);
+ }
+}
diff --git a/src/Avalonia.Controls/Automation/Provider/TextRange.cs b/src/Avalonia.Controls/Automation/Provider/TextRange.cs
new file mode 100644
index 0000000000..dd732fb691
--- /dev/null
+++ b/src/Avalonia.Controls/Automation/Provider/TextRange.cs
@@ -0,0 +1,74 @@
+using System;
+
+namespace Avalonia.Automation.Provider
+{
+ ///
+ /// Represents a range of text returned by an .
+ ///
+ public readonly struct TextRange : IEquatable
+ {
+ ///
+ /// Instantiates a new instance with the specified start index and
+ /// length.
+ ///
+ /// The inclusive start index of the range.
+ /// The length of the range.
+ ///
+ /// was negative.
+ ///
+ public TextRange(int start, int length)
+ {
+ if (length < 0)
+ throw new ArgumentException("Length may not be negative", nameof(length));
+ Start = start;
+ Length = length;
+ }
+
+ ///
+ /// Gets the inclusive start index of the range.
+ ///
+ public int Start { get; }
+
+ ///
+ /// Gets the exclusive end index of the range.
+ ///
+ public int End => Start + Length;
+
+ ///
+ /// Gets the length of the range.
+ ///
+ public int Length { get; }
+
+ ///
+ /// Gets an empty .
+ ///
+ public static TextRange Empty => new(0, 0);
+
+ ///
+ /// Creates a new from an inclusive start and end index.
+ ///
+ /// The inclusive start index of the range.
+ /// The inclusive end index of the range.
+ ///
+ public static TextRange FromInclusiveStartEnd(int start, int end)
+ {
+ var s = Math.Min(start, end);
+ var e = Math.Max(start, end);
+ return new(s, e - s);
+ }
+
+ public override bool Equals(object? obj) => obj is TextRange range && Equals(range);
+ public bool Equals(TextRange other) => Start == other.Start && Length == other.Length;
+
+ public override int GetHashCode()
+ {
+ var hashCode = -1730557556;
+ hashCode = hashCode * -1521134295 + Start.GetHashCode();
+ hashCode = hashCode * -1521134295 + Length.GetHashCode();
+ return hashCode;
+ }
+
+ public static bool operator ==(TextRange left, TextRange right) => left.Equals(right);
+ public static bool operator !=(TextRange left, TextRange right) => !(left == right);
+ }
+}
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 7bc26bf3b0..ff391e038f 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -798,6 +798,9 @@ namespace Avalonia.Controls
remove => RemoveHandler(TextChangingEvent, value);
}
+ internal TextPresenter? Presenter => _presenter;
+ internal IScrollable? Scroll { get; private set; }
+
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_presenter = e.NameScope.Get("PART_TextPresenter");
diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Text.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Text.cs
new file mode 100644
index 0000000000..17468c3c5b
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Text.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Automation.Provider;
+using UIA = Avalonia.Win32.Interop.Automation;
+
+#nullable enable
+
+namespace Avalonia.Win32.Automation
+{
+ internal partial class AutomationNode : UIA.ITextProvider
+ {
+ public UIA.ITextRangeProvider DocumentRange
+ {
+ get
+ {
+ var range = InvokeSync(x => x.DocumentRange);
+ return new AutomationTextRange(this, range);
+ }
+ }
+
+ public UIA.SupportedTextSelection SupportedTextSelection
+ {
+ get => (UIA.SupportedTextSelection)InvokeSync(x => x.SupportedTextSelection);
+ }
+
+ public UIA.ITextRangeProvider[] GetVisibleRanges() => GetRanges(x => x.GetVisibleRanges());
+
+ public UIA.ITextRangeProvider? RangeFromChild(UIA.IRawElementProviderSimple childElement) => null;
+
+ public UIA.ITextRangeProvider? RangeFromPoint(Point screenLocation)
+ {
+ var p = ToClient(screenLocation);
+ var range = InvokeSync(x => x.RangeFromPoint(screenLocation));
+ return new AutomationTextRange(this, range);
+ }
+
+ UIA.ITextRangeProvider[] UIA.ITextProvider.GetSelection() => GetRanges(x => x.GetSelection());
+
+ private UIA.ITextRangeProvider[] GetRanges(Func> selector)
+ {
+ var source = InvokeSync(selector);
+ return source?.Select(x => new AutomationTextRange(this, x)).ToArray() ?? Array.Empty();
+ }
+
+ private void InitializeTextProvider()
+ {
+ if (Peer is ITextProvider provider)
+ {
+ provider.SelectedRangesChanged += PeerSelectedTextRangesChanged;
+ provider.TextChanged += PeerTextChanged;
+ }
+ }
+
+ private void PeerSelectedTextRangesChanged(object? sender, EventArgs e)
+ {
+ RaiseEvent(UIA.UiaEventId.Text_TextSelectionChanged);
+ }
+
+ private void PeerTextChanged(object? sender, EventArgs e)
+ {
+ RaiseEvent(UIA.UiaEventId.Text_TextChanged);
+ }
+ }
+}
diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs
index 3eeedc4b5d..308acd7d22 100644
--- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs
+++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs
@@ -54,6 +54,7 @@ namespace Avalonia.Win32.Automation
_runtimeId = new int[] { 3, GetHashCode() };
Peer = peer;
s_nodes.Add(peer, this);
+ InitializeTextProvider();
peer.ChildrenChanged += Peer_ChildrenChanged;
peer.PropertyChanged += Peer_PropertyChanged;
}
@@ -118,6 +119,7 @@ namespace Avalonia.Win32.Automation
UiaPatternId.ScrollItem => this,
UiaPatternId.Selection => ThisIfPeerImplementsProvider(),
UiaPatternId.SelectionItem => ThisIfPeerImplementsProvider(),
+ UiaPatternId.Text => ThisIfPeerImplementsProvider(),
UiaPatternId.Toggle => ThisIfPeerImplementsProvider(),
UiaPatternId.Value => ThisIfPeerImplementsProvider(),
_ => null,
@@ -137,6 +139,7 @@ namespace Avalonia.Win32.Automation
UiaPropertyId.Culture => CultureInfo.CurrentCulture.LCID,
UiaPropertyId.FrameworkId => "Avalonia",
UiaPropertyId.HasKeyboardFocus => InvokeSync(() => Peer.HasKeyboardFocus()),
+ UiaPropertyId.HelpText => GetHelpText(),
UiaPropertyId.IsContentElement => InvokeSync(() => Peer.IsContentElement()),
UiaPropertyId.IsControlElement => InvokeSync(() => Peer.IsControlElement()),
UiaPropertyId.IsEnabled => InvokeSync(() => Peer.IsEnabled()),
@@ -192,13 +195,23 @@ namespace Avalonia.Win32.Automation
return peer is null ? null : s_nodes.GetValue(peer, Create);
}
- public static void Release(AutomationPeer peer) => s_nodes.Remove(peer);
+ public AutomationNode? GetRoot()
+ {
+ Dispatcher.UIThread.VerifyAccess();
- IRawElementProviderSimple[]? IRawElementProviderFragment.GetEmbeddedFragmentRoots() => null;
- void IRawElementProviderSimple2.ShowContextMenu() => InvokeSync(() => Peer.ShowContextMenu());
- void IInvokeProvider.Invoke() => InvokeSync((AAP.IInvokeProvider x) => x.Invoke());
+ var peer = Peer;
+ var parent = peer.GetParent();
+
+ while (peer.GetProvider() is null && parent is object)
+ {
+ peer = parent;
+ parent = peer.GetParent();
+ }
+
+ return peer is object ? GetOrCreate(peer) : null;
+ }
- protected void InvokeSync(Action action)
+ public void InvokeSync(Action action)
{
if (Dispatcher.UIThread.CheckAccess())
action();
@@ -206,7 +219,7 @@ namespace Avalonia.Win32.Automation
Dispatcher.UIThread.InvokeAsync(action).Wait();
}
- protected T InvokeSync(Func func)
+ public T InvokeSync(Func func)
{
if (Dispatcher.UIThread.CheckAccess())
return func();
@@ -214,7 +227,7 @@ namespace Avalonia.Win32.Automation
return Dispatcher.UIThread.InvokeAsync(func).Result;
}
- protected void InvokeSync(Action action)
+ public void InvokeSync(Action action)
{
if (Peer.GetProvider() is TInterface i)
{
@@ -233,7 +246,7 @@ namespace Avalonia.Win32.Automation
}
}
- protected TResult InvokeSync(Func func)
+ public TResult InvokeSync(Func func)
{
if (Peer.GetProvider() is TInterface i)
{
@@ -250,21 +263,40 @@ namespace Avalonia.Win32.Automation
throw new NotSupportedException();
}
+ public Point ToClient(Point p)
+ {
+ return ToClient(PixelPoint.FromPoint(p, 1));
+ }
- private AutomationNode? GetRoot()
+ public Point ToClient(PixelPoint p)
{
- Dispatcher.UIThread.VerifyAccess();
+ return (GetRoot() as RootAutomationNode)?.ToClient(p) ?? default;
+ }
- var peer = Peer;
- var parent = peer.GetParent();
+ public static void Release(AutomationPeer peer) => s_nodes.Remove(peer);
- while (peer.GetProvider() is null && parent is object)
- {
- peer = parent;
- parent = peer.GetParent();
- }
+ IRawElementProviderSimple[]? IRawElementProviderFragment.GetEmbeddedFragmentRoots() => null;
+ void IRawElementProviderSimple2.ShowContextMenu() => InvokeSync(() => Peer.ShowContextMenu());
+ void IInvokeProvider.Invoke() => InvokeSync((AAP.IInvokeProvider x) => x.Invoke());
- return peer is object ? GetOrCreate(peer) : null;
+ protected void RaiseEvent(UiaEventId eventId)
+ {
+ UiaCoreProviderApi.UiaRaiseAutomationEvent(this, (int)eventId);
+ }
+
+ protected void RaiseEvent(AutomationNode node, UiaEventId eventId)
+ {
+ UiaCoreProviderApi.UiaRaiseAutomationEvent(node, (int)eventId);
+ }
+
+ private string? GetHelpText()
+ {
+ return InvokeSync(() =>
+ {
+ // Placeholder is exposed via HelpText on UIA but help text and placeholder are two
+ // separate properties on macOS. Try both of them here.
+ return Peer.GetProvider()?.PlaceholderText ?? Peer.GetHelpText();
+ });
}
private static AutomationNode Create(AutomationPeer peer)
diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationTextRange.cs b/src/Windows/Avalonia.Win32/Automation/AutomationTextRange.cs
new file mode 100644
index 0000000000..66dffc3cec
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/Automation/AutomationTextRange.cs
@@ -0,0 +1,474 @@
+using System;
+using System.Runtime.InteropServices;
+using Avalonia.Automation.Provider;
+using Avalonia.Utilities;
+using Avalonia.Win32.Interop.Automation;
+using AAP = Avalonia.Automation.Provider;
+
+#nullable enable
+
+namespace Avalonia.Win32.Automation
+{
+ internal class AutomationTextRange : ITextRangeProvider
+ {
+ private readonly AutomationNode _owner;
+
+ public AutomationTextRange(AutomationNode owner, TextRange range)
+ : this(owner, range.Start, range.End)
+ {
+ }
+
+ public AutomationTextRange(AutomationNode owner, int start, int end)
+ {
+ _owner = owner;
+ Start = start;
+ End = end;
+ }
+
+ public int Start { get; private set; }
+ public int End { get; private set; }
+ public TextRange Range => TextRange.FromInclusiveStartEnd(Start, End);
+
+ private AAP.ITextProvider InnerProvider => (AAP.ITextProvider)_owner.Peer;
+
+ public ITextRangeProvider Clone() => new AutomationTextRange(_owner, Range);
+
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public bool Compare(ITextRangeProvider range)
+ {
+ return range is AutomationTextRange other && other.Start == Start && other.End == End;
+ }
+
+ public int CompareEndpoints(
+ TextPatternRangeEndpoint endpoint,
+ ITextRangeProvider targetRange,
+ TextPatternRangeEndpoint targetEndpoint)
+ {
+ var other = targetRange as AutomationTextRange ?? throw new InvalidOperationException("Invalid text range");
+ var e1 = (endpoint == TextPatternRangeEndpoint.Start) ? Start : End;
+ var e2 = (targetEndpoint == TextPatternRangeEndpoint.Start) ? other.Start : other.End;
+ return e1 - e2;
+ }
+
+ public void ExpandToEnclosingUnit(TextUnit unit)
+ {
+ _owner.InvokeSync(() =>
+ {
+ var text = InnerProvider.GetText(InnerProvider.DocumentRange);
+
+ switch (unit)
+ {
+ case TextUnit.Character:
+ if (Start == End)
+ End = MoveEndpointForward(text, End, TextUnit.Character, 1, out _);
+ break;
+ case TextUnit.Word:
+ // Move start left until we reach a word boundary.
+ for (; !AtWordBoundary(text, Start); Start--)
+ ;
+ // Move end right until we reach word boundary (different from Start).
+ End = Math.Min(Math.Max(End, Start + 1), text.Length);
+ for (; !AtWordBoundary(text, End); End++)
+ ;
+ break;
+ case TextUnit.Line:
+ if (InnerProvider.LineCount != 1)
+ {
+ int startLine = InnerProvider.GetLineForIndex(Start);
+ int endLine = InnerProvider.GetLineForIndex(End);
+ var start = InnerProvider.GetLineRange(startLine).Start;
+ var end = InnerProvider.GetLineRange(endLine).End;
+ MoveTo(start, end);
+ }
+ else
+ {
+ MoveTo(0, text.Length);
+ }
+ break;
+ case TextUnit.Paragraph:
+ // Move start left until we reach a paragraph boundary.
+ for (; !AtParagraphBoundary(text, Start); Start--)
+ ;
+ // Move end right until we reach a paragraph boundary (different from Start).
+ End = Math.Min(Math.Max(End, Start + 1), text.Length);
+ for (; !AtParagraphBoundary(text, End); End++)
+ ;
+ break;
+ case TextUnit.Format:
+ case TextUnit.Page:
+ case TextUnit.Document:
+ MoveTo(0, text.Length);
+ break;
+ default:
+ throw new ArgumentException("Invalid TextUnit.", nameof(unit));
+ }
+ });
+ }
+
+ public ITextRangeProvider? FindAttribute(
+ TextPatternAttribute attribute,
+ object? value,
+ [MarshalAs(UnmanagedType.Bool)] bool backward)
+ {
+ // TODO: Implement.
+ return null;
+ }
+
+ public ITextRangeProvider? FindText(
+ string text,
+ [MarshalAs(UnmanagedType.Bool)] bool backward,
+ [MarshalAs(UnmanagedType.Bool)] bool ignoreCase)
+ {
+ return _owner.InvokeSync(() =>
+ {
+ var rangeText = InnerProvider.GetText(Range);
+
+ if (ignoreCase)
+ {
+ rangeText = rangeText.ToLowerInvariant();
+ text = text.ToLowerInvariant();
+ }
+
+ var i = backward ?
+ rangeText.LastIndexOf(text, StringComparison.Ordinal) :
+ rangeText.IndexOf(text, StringComparison.Ordinal);
+ return i >= 0 ? new AutomationTextRange(_owner, Start + i, Start + i + text.Length) : null;
+ });
+ }
+
+ public object? GetAttributeValue(TextPatternAttribute attribute)
+ {
+ return attribute switch
+ {
+ TextPatternAttribute.IsReadOnlyAttributeId => _owner.InvokeSync(() => InnerProvider.IsReadOnly),
+ _ => null
+ };
+ }
+
+ public double[] GetBoundingRectangles()
+ {
+ return _owner.InvokeSync(() =>
+ {
+ var rects = InnerProvider.GetBounds(Range);
+ var result = new double[rects.Count * 4];
+ var root = _owner.GetRoot() as RootAutomationNode;
+
+ if (root is object)
+ {
+ for (var i = 0; i < rects.Count; i++)
+ {
+ var screenRect = root.ToScreen(rects[i]);
+ result[4 * i] = screenRect.X;
+ result[4 * i + 1] = screenRect.Y;
+ result[4 * i + 2] = screenRect.Width;
+ result[4 * i + 3] = screenRect.Height;
+ }
+ }
+
+ return result;
+ });
+ }
+
+ public IRawElementProviderSimple[] GetChildren() => Array.Empty();
+ public IRawElementProviderSimple GetEnclosingElement() => _owner;
+
+ public string GetText(int maxLength)
+ {
+ if (maxLength < 0)
+ maxLength = int.MaxValue;
+ maxLength = Math.Min(maxLength, End - Start);
+ return _owner.InvokeSync(() => InnerProvider.GetText(new TextRange(Start, maxLength)));
+ }
+
+ public int Move(TextUnit unit, int count)
+ {
+ return _owner.InvokeSync(() =>
+ {
+ if (count == 0)
+ return 0;
+ var text = InnerProvider.GetText(new(0, int.MaxValue));
+ // Save the start and end in case the move fails.
+ var oldStart = Start;
+ var oldEnd = End;
+ var wasDegenerate = Start == End;
+ // Move the start of the text range forward or backward in the document by the
+ // requested number of text unit boundaries.
+ var moved = MoveEndpointByUnit(TextPatternRangeEndpoint.Start, unit, count);
+ var succeeded = moved != 0;
+ if (succeeded)
+ {
+ // Make the range degenerate at the new start point.
+ End = Start;
+ // If we previously had a non-degenerate range then expand the range.
+ if (!wasDegenerate)
+ {
+ var forwards = count > 0;
+ if (forwards && Start == text.Length - 1)
+ {
+ // The start is at the end of the document, so move the start backward by
+ // one text unit to expand the text range from the degenerate range state.
+ Start = MoveEndpointBackward(text, Start, unit, -1, out var expandMoved);
+ --moved;
+ succeeded = expandMoved == -1 && moved > 0;
+ }
+ else
+ {
+ // The start is not at the end of the document, so move the endpoint
+ // forward by one text unit to expand the text range from the degenerate
+ // state.
+ End = MoveEndpointForward(text, End, unit, 1, out var expandMoved);
+ succeeded = expandMoved > 0;
+ }
+ }
+ }
+ if (!succeeded)
+ {
+ Start = oldStart;
+ End = oldEnd;
+ moved = 0;
+ }
+ return moved;
+ });
+ }
+
+ public void MoveEndpointByRange(TextPatternRangeEndpoint endpoint, ITextRangeProvider targetRange, TextPatternRangeEndpoint targetEndpoint)
+ {
+ var editRange = targetRange as AutomationTextRange ?? throw new InvalidOperationException("Invalid text range");
+ var e = (targetEndpoint == TextPatternRangeEndpoint.Start) ? editRange.Start : editRange.End;
+
+ if (endpoint == TextPatternRangeEndpoint.Start)
+ Start = e;
+ else
+ End = e;
+ }
+
+ public int MoveEndpointByUnit(TextPatternRangeEndpoint endpoint, TextUnit unit, int count)
+ {
+ if (count == 0)
+ return 0;
+
+ return _owner.InvokeSync(() =>
+ {
+ var text = InnerProvider.GetText(InnerProvider.DocumentRange);
+ var moved = 0;
+ var moveStart = endpoint == TextPatternRangeEndpoint.Start;
+
+ if (count > 0)
+ {
+ if (moveStart)
+ Start = MoveEndpointForward(text, Start, unit, count, out moved);
+ else
+ End = MoveEndpointForward(text, End, unit, count, out moved);
+ }
+ else if (count < 0)
+ {
+ if (moveStart)
+ Start = MoveEndpointBackward(text, Start, unit, count, out moved);
+ else
+ End = MoveEndpointBackward(text, End, unit, count, out moved);
+ }
+
+ return moved;
+ });
+ }
+
+ void ITextRangeProvider.AddToSelection() => throw new NotSupportedException();
+ void ITextRangeProvider.RemoveFromSelection() => throw new NotSupportedException();
+
+ public void ScrollIntoView([MarshalAs(UnmanagedType.Bool)] bool alignToTop)
+ {
+ _owner.InvokeSync(() => InnerProvider.ScrollIntoView(Range));
+ }
+
+ public void Select()
+ {
+ _owner.InvokeSync(() => InnerProvider.Select(Range));
+ }
+
+ private static bool AtParagraphBoundary(string text, int index)
+ {
+ return index <= 0 || index >= text.Length || (text[index] == '\r') && (text[index] != '\n');
+ }
+
+ // returns true iff index identifies a word boundary within text.
+ // following richedit & word precedent the boundaries are at the leading edge of the word
+ // so the span of a word includes trailing whitespace.
+ private static bool AtWordBoundary(string text, int index)
+ {
+ // we are at a word boundary if we are at the beginning or end of the text
+ if (index <= 0 || index >= text.Length)
+ {
+ return true;
+ }
+
+ if (AtParagraphBoundary(text, index))
+ {
+ return true;
+ }
+
+ var ch1 = text[index - 1];
+ var ch2 = text[index];
+
+ // an apostrophe does *not* break a word if it follows or precedes characters
+ if ((char.IsLetterOrDigit(ch1) && IsApostrophe(ch2))
+ || (IsApostrophe(ch1) && char.IsLetterOrDigit(ch2) && index >= 2 && char.IsLetterOrDigit(text[index - 2])))
+ {
+ return false;
+ }
+
+ // the following transitions mark boundaries.
+ // note: these are constructed to include trailing whitespace.
+ return (char.IsWhiteSpace(ch1) && !char.IsWhiteSpace(ch2))
+ || (char.IsLetterOrDigit(ch1) && !char.IsLetterOrDigit(ch2))
+ || (!char.IsLetterOrDigit(ch1) && char.IsLetterOrDigit(ch2))
+ || (char.IsPunctuation(ch1) && char.IsWhiteSpace(ch2));
+ }
+
+ private static bool IsApostrophe(char ch)
+ {
+ return ch == '\'' ||
+ ch == (char)0x2019; // Unicode Right Single Quote Mark
+ }
+
+ private static bool IsWordBreak(char ch)
+ {
+ return char.IsWhiteSpace(ch) || char.IsPunctuation(ch);
+ }
+
+ private int MoveEndpointForward(string text, int index, TextUnit unit, int count, out int moved)
+ {
+ var limit = text.Length;
+
+ switch (unit)
+ {
+ case TextUnit.Character:
+ moved = Math.Min(count, limit - index);
+ index = index + moved;
+ index = index > limit ? limit : index;
+ break;
+
+ case TextUnit.Word:
+ // TODO: This will need to implement the Unicode word boundaries spec.
+ for (moved = 0; moved < count && index < text.Length; moved++)
+ {
+ for (index++; !AtWordBoundary(text, index); index++)
+ ;
+ for (; index < text.Length && IsWordBreak(text[index]); index++)
+ ;
+ }
+ break;
+
+ case TextUnit.Line:
+ {
+ var line = InnerProvider.GetLineForIndex(index);
+ var newLine = MathUtilities.Clamp(line + count, 0, InnerProvider.LineCount - 1);
+ index = InnerProvider.GetLineRange(newLine).Start;
+ moved = newLine - line;
+ }
+ break;
+
+ case TextUnit.Paragraph:
+ for (moved = 0; moved < count && index < text.Length; moved++)
+ {
+ for (index++; !AtParagraphBoundary(text, index); index++)
+ ;
+ }
+ break;
+
+ case TextUnit.Format:
+ case TextUnit.Page:
+ case TextUnit.Document:
+ moved = index < limit ? 1 : 0;
+ index = limit;
+ break;
+
+ default:
+ throw new ArgumentException("Invalid TextUnit.", nameof(unit));
+ }
+
+ return index;
+ }
+
+ // moves an endpoint backward a certain number of units.
+ // the endpoint is just an index into the text so it could represent either
+ // the endpoint.
+ private int MoveEndpointBackward(string text, int index, TextUnit unit, int count, out int moved)
+ {
+ if (index == 0)
+ {
+ moved = 0;
+ return 0;
+ }
+ switch (unit)
+ {
+ case TextUnit.Character:
+ int oneBasedIndex = index + 1;
+ moved = Math.Max(count, -oneBasedIndex);
+ index += moved;
+ index = index < 0 ? 0 : index;
+ break;
+ case TextUnit.Word:
+ for (moved = 0; moved > count && index > 0; moved--)
+ {
+ for (index--; index < text.Length && IsWordBreak(text[index]); index--)
+ ;
+ for (index--; !AtWordBoundary(text, index); index--)
+ ;
+ }
+ break;
+ case TextUnit.Line:
+ {
+ var line = InnerProvider.GetLineForIndex(index);
+ // If a line other than the first consists of only a newline, then you can
+ // move backwards past this line and the position changes, hence this is
+ // counted. The first line is special, though: if it is empty, and you move
+ // say from the second line back up to the first, you cannot move further.
+ // However if the first line is nonempty, you can move from the end of the
+ // first line to its beginning! This latter move is counted, but if the
+ // first line is empty, it is not counted.
+ if (line == 0)
+ {
+ index = 0;
+ moved = !IsEol(text[0]) ? -1 : 0;
+ }
+ else
+ {
+ var newLine = MathUtilities.Clamp(line + count, 0, InnerProvider.LineCount - 1);
+ index = InnerProvider.GetLineRange(newLine).Start;
+ moved = newLine - line;
+ }
+ }
+ break;
+ case TextUnit.Paragraph:
+ for (moved = 0; moved > count && index > 0; moved--)
+ {
+ for (index--; !AtParagraphBoundary(text, index); index--)
+ ;
+ }
+ break;
+ case TextUnit.Format:
+ case TextUnit.Page:
+ case TextUnit.Document:
+ moved = index > 0 ? -1 : 0;
+ index = 0;
+ break;
+ default:
+ throw new ArgumentException("Invalid TextUnit.", nameof(unit));
+ }
+ return index;
+ }
+
+ private void MoveTo(int start, int end)
+ {
+ if (start < 0 || end < start)
+ throw new InvalidOperationException();
+ Start = start;
+ End = end;
+ }
+
+ public static bool IsEol(char c)
+ {
+ return c == '\r' || c == '\n';
+ }
+ }
+}
diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs
index ff8ff69d5e..9f0a4bfa61 100644
--- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs
+++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs
@@ -77,7 +77,15 @@ namespace Avalonia.Win32.Automation
public void FocusChanged(object? sender, EventArgs e)
{
- RaiseFocusChanged(GetOrCreate(Peer.GetFocus()));
+ if (GetOrCreate(Peer.GetFocus()) is AutomationNode node)
+ RaiseEvent(node, UiaEventId.AutomationFocusChanged);
+ }
+
+ public new Point ToClient(PixelPoint p)
+ {
+ if (WindowImpl is null)
+ return default;
+ return WindowImpl.PointToClient(p);
}
public Rect ToScreen(Rect rect)
diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/ITextProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/ITextProvider.cs
index 3f8fbc80c7..9700da3cea 100644
--- a/src/Windows/Avalonia.Win32/Interop/Automation/ITextProvider.cs
+++ b/src/Windows/Avalonia.Win32/Interop/Automation/ITextProvider.cs
@@ -20,8 +20,8 @@ namespace Avalonia.Win32.Interop.Automation
{
ITextRangeProvider [] GetSelection();
ITextRangeProvider [] GetVisibleRanges();
- ITextRangeProvider RangeFromChild(IRawElementProviderSimple childElement);
- ITextRangeProvider RangeFromPoint(Point screenLocation);
+ 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
index 9ebb4c9f49..7a875a747e 100644
--- a/src/Windows/Avalonia.Win32/Interop/Automation/ITextRangeProvider.cs
+++ b/src/Windows/Avalonia.Win32/Interop/Automation/ITextRangeProvider.cs
@@ -1,5 +1,7 @@
using System.Runtime.InteropServices;
+#nullable enable
+
namespace Avalonia.Win32.Interop.Automation
{
public enum TextPatternRangeEndpoint
@@ -19,21 +21,68 @@ namespace Avalonia.Win32.Interop.Automation
Document = 6,
}
+ public enum TextPatternAttribute
+ {
+ AnimationStyleAttributeId = 40000,
+ BackgroundColorAttributeId = 40001,
+ BulletStyleAttributeId = 40002,
+ CapStyleAttributeId = 40003,
+ CultureAttributeId = 40004,
+ FontNameAttributeId = 40005,
+ FontSizeAttributeId = 40006,
+ FontWeightAttributeId = 40007,
+ ForegroundColorAttributeId = 40008,
+ HorizontalTextAlignmentAttributeId = 40009,
+ IndentationFirstLineAttributeId = 40010,
+ IndentationLeadingAttributeId = 40011,
+ IndentationTrailingAttributeId = 40012,
+ IsHiddenAttributeId = 40013,
+ IsItalicAttributeId = 40014,
+ IsReadOnlyAttributeId = 40015,
+ IsSubscriptAttributeId = 40016,
+ IsSuperscriptAttributeId = 40017,
+ MarginBottomAttributeId = 40018,
+ MarginLeadingAttributeId = 40019,
+ MarginTopAttributeId = 40020,
+ MarginTrailingAttributeId = 40021,
+ OutlineStylesAttributeId = 40022,
+ OverlineColorAttributeId = 40023,
+ OverlineStyleAttributeId = 40024,
+ StrikethroughColorAttributeId = 40025,
+ StrikethroughStyleAttributeId = 40026,
+ TabsAttributeId = 40027,
+ TextFlowDirectionsAttributeId = 40028,
+ UnderlineColorAttributeId = 40029,
+ UnderlineStyleAttributeId = 40030,
+ AnnotationTypesAttributeId = 40031,
+ AnnotationObjectsAttributeId = 40032,
+ StyleNameAttributeId = 40033,
+ StyleIdAttributeId = 40034,
+ LinkAttributeId = 40035,
+ IsActiveAttributeId = 40036,
+ SelectionActiveEndAttributeId = 40037,
+ CaretPositionAttributeId = 40038,
+ CaretBidiModeAttributeId = 40039,
+ LineSpacingAttributeId = 40040,
+ BeforeParagraphSpacingAttributeId = 40041,
+ AfterParagraphSpacingAttributeId = 40042,
+ SayAsInterpretAsAttributeId = 40043,
+ }
+
[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();
+ ITextRangeProvider? FindAttribute(TextPatternAttribute attribute, object? value, [MarshalAs(UnmanagedType.Bool)] bool backward);
+ ITextRangeProvider? FindText(string text, [MarshalAs(UnmanagedType.Bool)] bool backward, [MarshalAs(UnmanagedType.Bool)] bool ignoreCase);
+ object? GetAttributeValue(TextPatternAttribute attribute);
+ double[] GetBoundingRectangles();
IRawElementProviderSimple GetEnclosingElement();
string GetText(int maxLength);
int Move(TextUnit unit, int count);