Skip to content

Commit f2baffc

Browse files
Implement TextPattern support for Text controls
- Implement base integration to AccessibleObject - Add TextPattern support for TextBox (singleline and multiline) and MaskedTextBox - Disable managed UiaTextProvider for RichTextBox to use native provider as the native Win32 RichEdit control already supports TextPattern. Return ControlAccessibleObject as AccessibilityInstance instead TextBoxBaseAccessibleObject (the latter supports managed UiaTextProvider). - Add tests Fixes #1588
1 parent 7545756 commit f2baffc

15 files changed

+1792
-48
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
~override System.Windows.Forms.RichTextBox.CreateAccessibilityInstance() -> System.Windows.Forms.AccessibleObject
2+
~override System.Windows.Forms.TextBox.OnKeyUp(System.Windows.Forms.KeyEventArgs e) -> void
3+
~override System.Windows.Forms.TextBox.OnMouseDown(System.Windows.Forms.MouseEventArgs e) -> void

src/System.Windows.Forms/src/System/Windows/Forms/AccessibleObject.cs

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ public partial class AccessibleObject :
4343
UiaCore.ISelectionItemProvider,
4444
UiaCore.IRawElementProviderHwndOverride,
4545
UiaCore.IScrollItemProvider,
46-
UiaCore.IMultipleViewProvider
46+
UiaCore.IMultipleViewProvider,
47+
UiaCore.ITextProvider,
48+
UiaCore.ITextProvider2
4749
{
4850
/// <summary>
4951
/// Specifies the <see cref='IAccessible'/> interface used by this <see cref='AccessibleObject'/>.
@@ -64,6 +66,9 @@ public partial class AccessibleObject :
6466
// Indicates this object is being used ONLY to wrap a system IAccessible
6567
private readonly bool systemWrapper;
6668

69+
private UiaTextProvider? _textProvider;
70+
private UiaTextProvider2? _textProvider2;
71+
6772
// The support for the UIA Notification event begins in RS3.
6873
// Assume the UIA Notification event is available until we learn otherwise.
6974
// If we learn that the UIA Notification event is not available,
@@ -350,15 +355,13 @@ internal virtual int ProviderOptions
350355

351356
internal virtual UiaCore.IRawElementProviderSimple? HostRawElementProvider => null;
352357

353-
internal virtual object? GetPropertyValue(UiaCore.UIA propertyID)
354-
{
355-
if (propertyID == UiaCore.UIA.IsInvokePatternAvailablePropertyId)
358+
internal virtual object? GetPropertyValue(UiaCore.UIA propertyID) =>
359+
propertyID switch
356360
{
357-
return IsInvokePatternAvailable;
358-
}
359-
360-
return null;
361-
}
361+
UiaCore.UIA.IsInvokePatternAvailablePropertyId => IsInvokePatternAvailable,
362+
UiaCore.UIA.BoundingRectanglePropertyId => Bounds,
363+
_ => null
364+
};
362365

363366
private bool IsInvokePatternAvailable
364367
{
@@ -474,6 +477,29 @@ internal virtual void Toggle()
474477

475478
internal virtual void Invoke() => DoDefaultAction();
476479

480+
internal virtual UiaCore.ITextRangeProvider? DocumentRangeInternal => _textProvider?.DocumentRange;
481+
482+
internal virtual UiaCore.ITextRangeProvider[]? GetTextSelection() => _textProvider?.GetSelection();
483+
484+
internal virtual UiaCore.ITextRangeProvider[]? GetTextVisibleRanges() => _textProvider?.GetVisibleRanges();
485+
486+
internal virtual UiaCore.ITextRangeProvider? GetTextRangeFromChild(UiaCore.IRawElementProviderSimple childElement)
487+
=> _textProvider?.RangeFromChild(childElement);
488+
489+
internal virtual UiaCore.ITextRangeProvider? GetTextRangeFromPoint(Point screenLocation) => _textProvider?.RangeFromPoint(screenLocation);
490+
491+
internal virtual UiaCore.SupportedTextSelection SupportedTextSelectionInternal
492+
=> _textProvider?.SupportedTextSelection ?? UiaCore.SupportedTextSelection.None;
493+
494+
internal virtual UiaCore.ITextRangeProvider? GetTextCaretRange(out BOOL isActive)
495+
{
496+
isActive = BOOL.FALSE;
497+
return _textProvider2?.GetCaretRange(out isActive);
498+
}
499+
500+
internal virtual UiaCore.ITextRangeProvider? GetRangeFromAnnotation(UiaCore.IRawElementProviderSimple annotationElement) =>
501+
_textProvider2?.RangeFromAnnotation(annotationElement);
502+
477503
internal virtual bool IsReadOnly => false;
478504

479505
internal virtual void SetValue(string? newValue)
@@ -673,6 +699,37 @@ UiaCore.IRawElementProviderSimple[] UiaCore.ILegacyIAccessibleProvider.GetSelect
673699

674700
void UiaCore.IInvokeProvider.Invoke() => Invoke();
675701

702+
UiaCore.ITextRangeProvider? UiaCore.ITextProvider.DocumentRange => DocumentRangeInternal;
703+
704+
UiaCore.ITextRangeProvider[]? UiaCore.ITextProvider.GetSelection() => GetTextSelection();
705+
706+
UiaCore.ITextRangeProvider[]? UiaCore.ITextProvider.GetVisibleRanges() => GetTextVisibleRanges();
707+
708+
UiaCore.ITextRangeProvider? UiaCore.ITextProvider.RangeFromChild(UiaCore.IRawElementProviderSimple childElement) =>
709+
GetTextRangeFromChild(childElement);
710+
711+
UiaCore.ITextRangeProvider? UiaCore.ITextProvider.RangeFromPoint(Point screenLocation) => GetTextRangeFromPoint(screenLocation);
712+
713+
UiaCore.SupportedTextSelection UiaCore.ITextProvider.SupportedTextSelection => SupportedTextSelectionInternal;
714+
715+
UiaCore.ITextRangeProvider? UiaCore.ITextProvider2.DocumentRange => DocumentRangeInternal;
716+
717+
UiaCore.ITextRangeProvider[]? UiaCore.ITextProvider2.GetSelection() => GetTextSelection();
718+
719+
UiaCore.ITextRangeProvider[]? UiaCore.ITextProvider2.GetVisibleRanges() => GetTextVisibleRanges();
720+
721+
UiaCore.ITextRangeProvider? UiaCore.ITextProvider2.RangeFromChild(UiaCore.IRawElementProviderSimple childElement) =>
722+
GetTextRangeFromChild(childElement);
723+
724+
UiaCore.ITextRangeProvider? UiaCore.ITextProvider2.RangeFromPoint(Point screenLocation) => GetTextRangeFromPoint(screenLocation);
725+
726+
UiaCore.SupportedTextSelection UiaCore.ITextProvider2.SupportedTextSelection => SupportedTextSelectionInternal;
727+
728+
UiaCore.ITextRangeProvider? UiaCore.ITextProvider2.GetCaretRange(out BOOL isActive) => GetTextCaretRange(out isActive);
729+
730+
UiaCore.ITextRangeProvider? UiaCore.ITextProvider2.RangeFromAnnotation(UiaCore.IRawElementProviderSimple annotationElement) =>
731+
GetRangeFromAnnotation(annotationElement);
732+
676733
BOOL UiaCore.IValueProvider.IsReadOnly => IsReadOnly ? BOOL.TRUE : BOOL.FALSE;
677734

678735
string? UiaCore.IValueProvider.Value => Value;
@@ -1595,6 +1652,12 @@ protected void UseStdAccessibleObjects(IntPtr handle, int objid)
15951652
}
15961653
}
15971654

1655+
internal void UseTextProviders(UiaTextProvider textProvider, UiaTextProvider2 textProvider2)
1656+
{
1657+
_textProvider = textProvider ?? throw new ArgumentNullException(nameof(textProvider));
1658+
_textProvider2 = textProvider2 ?? throw new ArgumentNullException(nameof(textProvider2));
1659+
}
1660+
15981661
/// <summary>
15991662
/// Performs custom navigation between parent/child/sibling accessible
16001663
/// objects. This is basically just a wrapper for GetSysChild(), that

src/System.Windows.Forms/src/System/Windows/Forms/InternalAccessibleObject.cs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System.Diagnostics.CodeAnalysis;
6+
using System.Drawing;
67
using System.Globalization;
78
using System.Reflection;
89
using System.Runtime.InteropServices;
@@ -40,7 +41,9 @@ internal sealed class InternalAccessibleObject :
4041
ISelectionItemProvider,
4142
IScrollItemProvider,
4243
IRawElementProviderHwndOverride,
43-
IMultipleViewProvider
44+
IMultipleViewProvider,
45+
ITextProvider,
46+
ITextProvider2
4447
{
4548
private IAccessible publicIAccessible; // AccessibleObject as IAccessible
4649
private readonly Oleaut32.IEnumVariant publicIEnumVariant; // AccessibleObject as Oleaut32.IEnumVariant
@@ -69,6 +72,8 @@ internal sealed class InternalAccessibleObject :
6972
private readonly IScrollItemProvider publicIScrollItemProvider; // AccessibleObject as IScrollItemProvider
7073
private readonly IRawElementProviderHwndOverride publicIRawElementProviderHwndOverride; // AccessibleObject as IRawElementProviderHwndOverride
7174
private readonly IMultipleViewProvider publicIMultiViewProvider; // AccessibleObject as IMultipleViewProvider
75+
private readonly ITextProvider publicITextProvider; // AccessibleObject as ITextProvider
76+
private readonly ITextProvider2 publicITextProvider2; // AccessibleObject as ITextProvider2
7277

7378
/// <summary>
7479
/// Create a new wrapper.
@@ -100,6 +105,8 @@ internal InternalAccessibleObject(AccessibleObject accessibleImplemention)
100105
publicIScrollItemProvider = (IScrollItemProvider)accessibleImplemention;
101106
publicIRawElementProviderHwndOverride = (IRawElementProviderHwndOverride)accessibleImplemention;
102107
publicIMultiViewProvider = (IMultipleViewProvider)accessibleImplemention;
108+
publicITextProvider = (ITextProvider)accessibleImplemention;
109+
publicITextProvider2 = (ITextProvider2)accessibleImplemention;
103110
// Note: Deliberately not holding onto AccessibleObject to enforce all access through the interfaces
104111
}
105112

@@ -314,6 +321,8 @@ ProviderOptions IRawElementProviderSimple.ProviderOptions
314321
UIA.SelectionItemPatternId => (ISelectionItemProvider)this,
315322
UIA.ScrollItemPatternId => (IScrollItemProvider)this,
316323
UIA.MultipleViewPatternId => (IMultipleViewProvider)this,
324+
UIA.TextPatternId => (ITextProvider)this,
325+
UIA.TextPattern2Id => (ITextProvider2)this,
317326
_ => null
318327
};
319328
}
@@ -365,8 +374,7 @@ UiaRect IRawElementProviderFragment.BoundingRectangle
365374

366375
void ILegacyIAccessibleProvider.DoDefaultAction() => publicILegacyIAccessibleProvider.DoDefaultAction();
367376

368-
IAccessible? ILegacyIAccessibleProvider.GetIAccessible()
369-
=> publicILegacyIAccessibleProvider.GetIAccessible();
377+
IAccessible? ILegacyIAccessibleProvider.GetIAccessible() => publicILegacyIAccessibleProvider.GetIAccessible();
370378

371379
IRawElementProviderSimple[] ILegacyIAccessibleProvider.GetSelection() => publicILegacyIAccessibleProvider.GetSelection();
372380

@@ -376,6 +384,40 @@ UiaRect IRawElementProviderFragment.BoundingRectangle
376384

377385
void IInvokeProvider.Invoke() => publicIInvokeProvider.Invoke();
378386

387+
ITextRangeProvider[]? ITextProvider.GetSelection() => publicITextProvider.GetSelection();
388+
389+
ITextRangeProvider[]? ITextProvider.GetVisibleRanges() => publicITextProvider.GetVisibleRanges();
390+
391+
ITextRangeProvider? ITextProvider.RangeFromChild(IRawElementProviderSimple childElement)
392+
=> publicITextProvider.RangeFromChild(childElement);
393+
394+
ITextRangeProvider? ITextProvider.RangeFromPoint(Point screenLocation)
395+
=> publicITextProvider.RangeFromPoint(screenLocation);
396+
397+
SupportedTextSelection ITextProvider.SupportedTextSelection => publicITextProvider.SupportedTextSelection;
398+
399+
ITextRangeProvider? ITextProvider.DocumentRange => publicITextProvider.DocumentRange;
400+
401+
ITextRangeProvider[]? ITextProvider2.GetSelection() => publicITextProvider2.GetSelection();
402+
403+
ITextRangeProvider[]? ITextProvider2.GetVisibleRanges() => publicITextProvider2.GetVisibleRanges();
404+
405+
ITextRangeProvider? ITextProvider2.RangeFromChild(IRawElementProviderSimple childElement)
406+
=> publicITextProvider2.RangeFromChild(childElement);
407+
408+
ITextRangeProvider? ITextProvider2.RangeFromPoint(Point screenLocation)
409+
=> publicITextProvider2.RangeFromPoint(screenLocation);
410+
411+
SupportedTextSelection ITextProvider2.SupportedTextSelection => publicITextProvider2.SupportedTextSelection;
412+
413+
ITextRangeProvider? ITextProvider2.DocumentRange => publicITextProvider2.DocumentRange;
414+
415+
ITextRangeProvider? ITextProvider2.GetCaretRange(out BOOL isActive)
416+
=> publicITextProvider2.GetCaretRange(out isActive);
417+
418+
ITextRangeProvider? ITextProvider2.RangeFromAnnotation(IRawElementProviderSimple annotationElement)
419+
=> publicITextProvider2.RangeFromAnnotation(annotationElement);
420+
379421
BOOL IValueProvider.IsReadOnly => publicIValueProvider.IsReadOnly;
380422

381423
string? IValueProvider.Value => publicIValueProvider.Value;

src/System.Windows.Forms/src/System/Windows/Forms/RichTextBox.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2257,7 +2257,7 @@ private RichTextBoxSelectionAttribute GetCharFormat(CFM mask, CFE effect)
22572257
return charFormat;
22582258
}
22592259

2260-
Font GetCharFormatFont(bool selectionOnly)
2260+
private Font GetCharFormatFont(bool selectionOnly)
22612261
{
22622262
ForceHandleCreate();
22632263

@@ -3237,6 +3237,8 @@ private void UserPreferenceChangedHandler(object o, UserPreferenceChangedEventAr
32373237
}
32383238
}
32393239

3240+
protected override AccessibleObject CreateAccessibilityInstance() => new ControlAccessibleObject(this);
3241+
32403242
/// <summary>
32413243
/// Creates the IRichEditOleCallback compatible object for handling RichEdit callbacks. For more
32423244
/// information look up the MSDN info on this interface. This is designed to be a back door of

src/System.Windows.Forms/src/System/Windows/Forms/TextBox.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,48 @@ protected override void OnHandleDestroyed(EventArgs e)
647647
base.OnHandleDestroyed(e);
648648
}
649649

650+
protected override void OnKeyUp(KeyEventArgs e)
651+
{
652+
base.OnKeyUp(e);
653+
654+
if (IsHandleCreated && ContainsNavigationKeyCode(e.KeyCode))
655+
{
656+
AccessibilityObject?.RaiseAutomationEvent(UiaCore.UIA.Text_TextSelectionChangedEventId);
657+
}
658+
}
659+
660+
private bool ContainsNavigationKeyCode(Keys keyCode)
661+
{
662+
switch (keyCode)
663+
{
664+
case Keys.Up:
665+
case Keys.Down:
666+
case Keys.PageUp:
667+
case Keys.PageDown:
668+
case Keys.Home:
669+
case Keys.End:
670+
case Keys.Left:
671+
case Keys.Right:
672+
return true;
673+
default:
674+
return false;
675+
}
676+
}
677+
678+
protected override void OnMouseDown(MouseEventArgs e)
679+
{
680+
base.OnMouseDown(e);
681+
682+
if (IsHandleCreated)
683+
{
684+
// As there is no corresponding windows notification
685+
// about text selection changed for TextBox assuming
686+
// that any mouse down on textbox leads to change of
687+
// the caret position and thereby change the selection.
688+
AccessibilityObject?.RaiseAutomationEvent(UiaCore.UIA.Text_TextSelectionChangedEventId);
689+
}
690+
}
691+
650692
protected virtual void OnTextAlignChanged(EventArgs e)
651693
{
652694
if (Events[EVENT_TEXTALIGNCHANGED] is EventHandler eh)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using static Interop;
6+
7+
namespace System.Windows.Forms
8+
{
9+
public abstract partial class TextBoxBase
10+
{
11+
internal class TextBoxBaseAccessibleObject : ControlAccessibleObject
12+
{
13+
private readonly TextBoxBase _owningTextBoxBase;
14+
private readonly TextBoxBaseUiaTextProvider _textProvider;
15+
16+
public TextBoxBaseAccessibleObject(TextBoxBase owner) : base(owner)
17+
{
18+
_owningTextBoxBase = owner;
19+
_textProvider = new TextBoxBaseUiaTextProvider(owner);
20+
21+
UseTextProviders(_textProvider, _textProvider);
22+
}
23+
24+
internal override bool IsIAccessibleExSupported() => true;
25+
26+
internal override bool IsPatternSupported(UiaCore.UIA patternId) =>
27+
patternId switch
28+
{
29+
UiaCore.UIA.TextPatternId => true,
30+
UiaCore.UIA.TextPattern2Id => true,
31+
_ => base.IsPatternSupported(patternId)
32+
};
33+
34+
internal override object? GetPropertyValue(UiaCore.UIA propertyID) =>
35+
propertyID switch
36+
{
37+
UiaCore.UIA.IsTextPatternAvailablePropertyId => IsPatternSupported(UiaCore.UIA.TextPatternId),
38+
UiaCore.UIA.IsTextPattern2AvailablePropertyId => IsPatternSupported(UiaCore.UIA.TextPattern2Id),
39+
UiaCore.UIA.IsValuePatternAvailablePropertyId => IsPatternSupported(UiaCore.UIA.ValuePatternId),
40+
_ => base.GetPropertyValue(propertyID)
41+
};
42+
43+
internal override bool IsReadOnly => _owningTextBoxBase.ReadOnly;
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)