-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fix UIA navigation for TabControl #5380
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| ~override System.Windows.Forms.TabControl.CreateAccessibilityInstance() -> System.Windows.Forms.AccessibleObject | ||
| ~override System.Windows.Forms.TabPage.CreateAccessibilityInstance() -> System.Windows.Forms.AccessibleObject |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| // 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. | ||
|
|
||
| using System.Drawing; | ||
| using static Interop.UiaCore; | ||
|
|
||
| namespace System.Windows.Forms | ||
| { | ||
| public partial class TabControl | ||
| { | ||
| internal class TabControlAccessibleObject : ControlAccessibleObject | ||
| { | ||
| private readonly TabControl _owningTabControl; | ||
|
|
||
| public TabControlAccessibleObject(TabControl owningTabControl) : base(owningTabControl) | ||
| { | ||
| _owningTabControl = owningTabControl; | ||
| } | ||
|
|
||
| public override Rectangle Bounds | ||
| { | ||
| get | ||
| { | ||
| if (!_owningTabControl.IsHandleCreated || GetSystemIAccessibleInternal() is null) | ||
| { | ||
| return Rectangle.Empty; | ||
| } | ||
|
|
||
| // The "NativeMethods.CHILDID_SELF" constant returns to the id of the TabPage, | ||
| // which allows to use the native "accLocation" method to get the "Bounds" property | ||
| GetSystemIAccessibleInternal()!.accLocation(out int left, out int top, out int width, out int height, NativeMethods.CHILDID_SELF); | ||
| return new(left, top, width, height); | ||
| } | ||
| } | ||
|
|
||
| public override AccessibleRole Role | ||
| => Owner.AccessibleRole != AccessibleRole.Default | ||
| ? Owner.AccessibleRole | ||
| : AccessibleRole.PageTabList; | ||
|
|
||
| public override AccessibleStates State | ||
| // The "NativeMethods.CHILDID_SELF" constant returns to the id of the trackbar, | ||
| // which allows to use the native "get_accState" method to get the "State" property | ||
| => GetSystemIAccessibleInternal()?.get_accState(NativeMethods.CHILDID_SELF) is object accState | ||
| ? (AccessibleStates)accState | ||
| : AccessibleStates.None; | ||
|
|
||
| internal override IRawElementProviderFragmentRoot FragmentRoot => this; | ||
|
|
||
| internal override bool IsSelectionRequired => true; | ||
SergeySmirnov-Akvelon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| public override AccessibleObject? GetChild(int index) | ||
| { | ||
| if (!_owningTabControl.IsHandleCreated | ||
dreddy-work marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| || _owningTabControl.TabPages.Count == 0 | ||
| || index < 0 | ||
| || index > _owningTabControl.TabPages.Count) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| return index == 0 | ||
| ? _owningTabControl.SelectedTab?.AccessibilityObject | ||
| : _owningTabControl.TabPages[index - 1].TabAccessibilityObject; | ||
| } | ||
|
|
||
| public override int GetChildCount() | ||
| { | ||
| if (!_owningTabControl.IsHandleCreated) | ||
| { | ||
| // We return -1 instead of 0 when the Handle has not been created, | ||
| // so that the user can distinguish between the situation | ||
| // when something went wrong (in this case, the Handle was not created) | ||
| // and the situation when the Handle was created, but the TabControl, | ||
| // for example, does not contain TabPages. | ||
| return -1; | ||
SergeySmirnov-Akvelon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| if (_owningTabControl.TabPages.Count == 0) | ||
| { | ||
| return 0; | ||
| } | ||
|
|
||
| // We add 1 to the number of TabPages, since the TabControl, in addition to the elements | ||
| // for the TabPages,contains an element for the Panel of the selected TabPage. | ||
| return _owningTabControl.TabPages.Count + 1; | ||
Tanya-Solyanik marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| public override AccessibleObject? HitTest(int x, int y) | ||
| { | ||
| if (!_owningTabControl.IsHandleCreated) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| Point point = new(x, y); | ||
| if (_owningTabControl.SelectedTab is not null | ||
| && _owningTabControl.SelectedTab.AccessibilityObject.Bounds.Contains(point)) | ||
| { | ||
| return _owningTabControl.SelectedTab.AccessibilityObject; | ||
| } | ||
|
|
||
| foreach (TabPage tabPage in _owningTabControl.TabPages) | ||
| { | ||
| if (tabPage.TabAccessibilityObject.Bounds.Contains(point)) | ||
| { | ||
| return tabPage.TabAccessibilityObject; | ||
| } | ||
| } | ||
|
|
||
|
||
| return this; | ||
| } | ||
|
|
||
| internal override IRawElementProviderFragment? ElementProviderFromPoint(double x, double y) | ||
| => HitTest((int)x, (int)y) ?? base.ElementProviderFromPoint(x, y); | ||
|
|
||
| internal override IRawElementProviderFragment? FragmentNavigate(NavigateDirection direction) | ||
| { | ||
| if (!_owningTabControl.IsHandleCreated) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| return direction switch | ||
| { | ||
| NavigateDirection.FirstChild => _owningTabControl.SelectedTab?.AccessibilityObject, | ||
| NavigateDirection.LastChild => _owningTabControl.TabPages.Count > 0 | ||
| ? _owningTabControl.TabPages[^1].TabAccessibilityObject | ||
| : null, | ||
| _ => base.FragmentNavigate(direction) | ||
| }; | ||
| } | ||
|
|
||
| internal override object? GetPropertyValue(UIA propertyID) | ||
| => propertyID switch | ||
SergeySmirnov-Akvelon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| UIA.RuntimeIdPropertyId => RuntimeId, | ||
| UIA.AutomationIdPropertyId => _owningTabControl.Name, | ||
SergeySmirnov-Akvelon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| UIA.IsEnabledPropertyId => _owningTabControl.Enabled, | ||
| UIA.IsOffscreenPropertyId => (State & AccessibleStates.Offscreen) == AccessibleStates.Offscreen, | ||
| UIA.HasKeyboardFocusPropertyId => _owningTabControl.Focused, | ||
| UIA.NamePropertyId => Name, | ||
SergeySmirnov-Akvelon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| UIA.AccessKeyPropertyId => KeyboardShortcut, | ||
| UIA.NativeWindowHandlePropertyId => _owningTabControl.InternalHandle, | ||
| UIA.IsSelectionPatternAvailablePropertyId => IsPatternSupported(UIA.SelectionPatternId), | ||
| UIA.IsLegacyIAccessiblePatternAvailablePropertyId => IsPatternSupported(UIA.LegacyIAccessiblePatternId), | ||
SergeySmirnov-Akvelon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| UIA.IsKeyboardFocusablePropertyId | ||
| // This is necessary for compatibility with MSAA proxy: | ||
| // IsKeyboardFocusable = true regardless the control is enabled/disabled. | ||
| => true, | ||
| _ => base.GetPropertyValue(propertyID) | ||
SergeySmirnov-Akvelon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
| internal override IRawElementProviderSimple[]? GetSelection() | ||
| => !_owningTabControl.IsHandleCreated | ||
| || _owningTabControl.SelectedTab is null | ||
| ? Array.Empty<IRawElementProviderSimple>() | ||
| : new IRawElementProviderSimple[] { _owningTabControl.SelectedTab.TabAccessibilityObject }; | ||
|
|
||
| internal override bool IsPatternSupported(UIA patternId) | ||
| => patternId switch | ||
| { | ||
| // The "Enabled" property of the TabControl does not affect the behavior of that property, | ||
| // so it is always true | ||
| UIA.SelectionPatternId => true, | ||
Tanya-Solyanik marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| _ => base.IsPatternSupported(patternId) | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -808,6 +808,8 @@ public bool ShowToolTips | |
| } | ||
| } | ||
|
|
||
| internal override bool SupportsUiaProviders => true; | ||
|
|
||
| /// <summary> | ||
| /// Returns the number of tabs in the strip | ||
| /// </summary> | ||
|
|
@@ -966,6 +968,8 @@ internal void BeginUpdate() | |
| BeginUpdateInternal(); | ||
| } | ||
|
|
||
| protected override AccessibleObject CreateAccessibilityInstance() => new TabControlAccessibleObject(this); | ||
|
|
||
| protected override Control.ControlCollection CreateControlsInstance() | ||
| { | ||
| return new ControlCollection(this); | ||
|
|
@@ -1278,6 +1282,11 @@ protected override void OnGotFocus(EventArgs e) | |
| { | ||
| NotifyAboutFocusState(SelectedTab, focused: true); | ||
| base.OnGotFocus(e); | ||
|
|
||
| if (IsAccessibilityObjectCreated && SelectedTab is not null) | ||
|
||
| { | ||
| SelectedTab.TabAccessibilityObject.RaiseAutomationEvent(UiaCore.UIA.AutomationFocusChangedEventId); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -2050,6 +2059,12 @@ private bool WmSelChange() | |
| { | ||
| OnSelected(new TabControlEventArgs(SelectedTab, SelectedIndex, TabControlAction.Selected)); | ||
| OnSelectedIndexChanged(EventArgs.Empty); | ||
|
|
||
| if (IsAccessibilityObjectCreated && SelectedTab?.ParentInternal is TabControl) | ||
| { | ||
| SelectedTab.TabAccessibilityObject.RaiseAutomationEvent(UiaCore.UIA.SelectionItem_ElementSelectedEventId); | ||
| SelectedTab.TabAccessibilityObject.RaiseAutomationEvent(UiaCore.UIA.AutomationFocusChangedEventId); | ||
| } | ||
| } | ||
| else | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| // 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. | ||
|
|
||
| using System.Drawing; | ||
| using Accessibility; | ||
| using static Interop; | ||
|
|
||
| namespace System.Windows.Forms | ||
| { | ||
| public partial class TabPage | ||
| { | ||
| internal class TabAccessibleObject : AccessibleObject | ||
SergeySmirnov-Akvelon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| private TabControl? OwningTabControl => _owningTabPage.ParentInternal as TabControl; | ||
|
|
||
|
||
| private readonly TabPage _owningTabPage; | ||
|
|
||
| public TabAccessibleObject(TabPage owningTabPage) | ||
| { | ||
| _owningTabPage = owningTabPage ?? throw new ArgumentNullException(nameof(owningTabPage)); | ||
| } | ||
|
|
||
| public override Rectangle Bounds | ||
| { | ||
| get | ||
| { | ||
| if (OwningTabControl is null || !OwningTabControl.IsHandleCreated || SystemIAccessibleInternal is null) | ||
| { | ||
| return Rectangle.Empty; | ||
| } | ||
|
|
||
| int index = CurrentIndex; | ||
|
|
||
| if (index == -1 || (State & AccessibleStates.Invisible) == AccessibleStates.Invisible) | ||
| { | ||
| return Rectangle.Empty; | ||
| } | ||
|
|
||
| // The "GetChildId" method returns to the id of the TabControl element, | ||
| // which allows to use the native "accLocation" method to get the "Bounds" property | ||
| SystemIAccessibleInternal.accLocation(out int left, out int top, out int width, out int height, GetChildId()); | ||
| return new(left, top, width, height); | ||
| } | ||
| } | ||
|
|
||
| public override string? DefaultAction => SystemIAccessibleInternal?.get_accDefaultAction(GetChildId()); | ||
|
|
||
| public override string? Name => _owningTabPage.Text; | ||
|
|
||
| public override AccessibleRole Role | ||
| => SystemIAccessibleInternal?.get_accRole(GetChildId()) is object accRole | ||
| ? (AccessibleRole)accRole | ||
| : AccessibleRole.None; | ||
|
|
||
| public override AccessibleStates State | ||
| => SystemIAccessibleInternal?.get_accState(GetChildId()) is object accState | ||
| ? (AccessibleStates)accState | ||
| : AccessibleStates.None; | ||
|
|
||
| internal override UiaCore.IRawElementProviderFragmentRoot? FragmentRoot => OwningTabControl?.AccessibilityObject; | ||
|
|
||
| internal override bool IsItemSelected => OwningTabControl?.SelectedTab == _owningTabPage; | ||
|
|
||
| internal override UiaCore.IRawElementProviderSimple? ItemSelectionContainer => OwningTabControl?.AccessibilityObject; | ||
|
|
||
| internal override int[]? RuntimeId | ||
| => new int[] | ||
| { | ||
| RuntimeIDFirstItem, | ||
| OwningTabControl is null | ||
| ? PARAM.ToInt(IntPtr.Zero) | ||
| : PARAM.ToInt(OwningTabControl.InternalHandle), | ||
| GetChildId() | ||
| }; | ||
|
|
||
| private int CurrentIndex => OwningTabControl?.TabPages.IndexOf(_owningTabPage) ?? -1; | ||
|
|
||
| private IAccessible? SystemIAccessibleInternal | ||
| => OwningTabControl?.AccessibilityObject.GetSystemIAccessibleInternal(); | ||
|
|
||
| public override void DoDefaultAction() | ||
| { | ||
| if (OwningTabControl is not null && OwningTabControl.IsHandleCreated && OwningTabControl.Enabled) | ||
| { | ||
| OwningTabControl.SelectedTab = _owningTabPage; | ||
| } | ||
| } | ||
|
|
||
| internal override void AddToSelection() => DoDefaultAction(); | ||
|
|
||
| internal override UiaCore.IRawElementProviderFragment? FragmentNavigate(UiaCore.NavigateDirection direction) | ||
SergeySmirnov-Akvelon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| if (OwningTabControl is null || !OwningTabControl.IsHandleCreated) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| return direction switch | ||
| { | ||
| UiaCore.NavigateDirection.Parent => OwningTabControl.AccessibilityObject, | ||
| UiaCore.NavigateDirection.NextSibling => OwningTabControl.AccessibilityObject.GetChild(GetChildId() + 1), | ||
| UiaCore.NavigateDirection.PreviousSibling => OwningTabControl.AccessibilityObject.GetChild(GetChildId() - 1), | ||
| _ => null | ||
| }; | ||
| } | ||
|
|
||
| // +1 is needed because 0 is the Pane id of the selected tab | ||
| internal override int GetChildId() => CurrentIndex + 1; | ||
|
|
||
| public override string? Help => SystemIAccessibleInternal?.get_accHelp(GetChildId()); | ||
|
|
||
| public override string? KeyboardShortcut => SystemIAccessibleInternal?.get_accKeyboardShortcut(GetChildId()); | ||
|
|
||
| internal override object? GetPropertyValue(UiaCore.UIA propertyID) | ||
| => propertyID switch | ||
| { | ||
| UiaCore.UIA.ControlTypePropertyId => UiaCore.UIA.TabItemControlTypeId, | ||
| UiaCore.UIA.RuntimeIdPropertyId => RuntimeId, | ||
| UiaCore.UIA.AutomationIdPropertyId => _owningTabPage.Name, | ||
| UiaCore.UIA.AccessKeyPropertyId => KeyboardShortcut ?? string.Empty, | ||
| UiaCore.UIA.IsPasswordPropertyId => false, | ||
| UiaCore.UIA.HelpTextPropertyId => Help ?? string.Empty, | ||
| UiaCore.UIA.IsEnabledPropertyId => OwningTabControl?.Enabled ?? false, | ||
| UiaCore.UIA.IsOffscreenPropertyId => (State & AccessibleStates.Offscreen) == AccessibleStates.Offscreen, | ||
| UiaCore.UIA.HasKeyboardFocusPropertyId => (State & AccessibleStates.Focused) == AccessibleStates.Focused, | ||
| UiaCore.UIA.NamePropertyId => Name, | ||
| UiaCore.UIA.IsSelectionItemPatternAvailablePropertyId => IsPatternSupported(UiaCore.UIA.SelectionItemPatternId), | ||
SergeySmirnov-Akvelon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| UiaCore.UIA.IsInvokePatternAvailablePropertyId => IsPatternSupported(UiaCore.UIA.InvokePatternId), | ||
| UiaCore.UIA.IsLegacyIAccessiblePatternAvailablePropertyId => IsPatternSupported(UiaCore.UIA.LegacyIAccessiblePatternId), | ||
| UiaCore.UIA.IsKeyboardFocusablePropertyId | ||
| // This is necessary for compatibility with MSAA proxy: | ||
| // IsKeyboardFocusable = true regardless the control is enabled/disabled. | ||
| => true, | ||
| _ => base.GetPropertyValue(propertyID) | ||
| }; | ||
|
|
||
| internal override bool IsPatternSupported(UiaCore.UIA patternId) | ||
| => patternId switch | ||
| { | ||
| // The "Enabled" property of the TabControl does not affect the behavior of that property, | ||
| // so it is always true | ||
| UiaCore.UIA.SelectionItemPatternId => true, | ||
| UiaCore.UIA.InvokePatternId => false, | ||
| UiaCore.UIA.LegacyIAccessiblePatternId => true, | ||
| _ => base.IsPatternSupported(patternId) | ||
| }; | ||
|
|
||
| internal override void RemoveFromSelection() | ||
| { | ||
| // Do nothing, C++ implementation returns UIA_E_INVALIDOPERATION 0x80131509 | ||
| } | ||
|
|
||
| internal unsafe override void SelectItem() => DoDefaultAction(); | ||
| } | ||
| } | ||
| } | ||

Uh oh!
There was an error while loading. Please reload this page.