Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/System.Windows.Forms/src/PublicAPI.Unshipped.txt
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;

public override AccessibleObject? GetChild(int index)
{
if (!_owningTabControl.IsHandleCreated
|| _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;
}

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;
}

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;
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when we hit the spinner control? Suppose we have more tab headers than fits into the control width, then we show a left/right arrows to navigate between the tab headers. Do we hit-test those arrows correctly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have investigated this question and did not find direct access to the Spinner from the TabControl. We are now using the native Spinner implementation. I have not seen any difference in behavior since .NET Framework 4.7.2, except that now the Spinner is displayed first in the list of control elements, and not second as in .NET Framework 4.7.2. How critical do you think this is?

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep the hierarchy as is and follow up with Ryan. We can address this separately.

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
{
UIA.RuntimeIdPropertyId => RuntimeId,
UIA.AutomationIdPropertyId => _owningTabControl.Name,
UIA.IsEnabledPropertyId => _owningTabControl.Enabled,
UIA.IsOffscreenPropertyId => (State & AccessibleStates.Offscreen) == AccessibleStates.Offscreen,
UIA.HasKeyboardFocusPropertyId => _owningTabControl.Focused,
UIA.NamePropertyId => Name,
UIA.AccessKeyPropertyId => KeyboardShortcut,
UIA.NativeWindowHandlePropertyId => _owningTabControl.InternalHandle,
UIA.IsSelectionPatternAvailablePropertyId => IsPatternSupported(UIA.SelectionPatternId),
UIA.IsLegacyIAccessiblePatternAvailablePropertyId => IsPatternSupported(UIA.LegacyIAccessiblePatternId),
UIA.IsKeyboardFocusablePropertyId
// This is necessary for compatibility with MSAA proxy:
// IsKeyboardFocusable = true regardless the control is enabled/disabled.
=> true,
_ => base.GetPropertyValue(propertyID)
};

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,
_ => base.IsPatternSupported(patternId)
};
}
}
}
15 changes: 15 additions & 0 deletions src/System.Windows.Forms/src/System/Windows/Forms/TabControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,8 @@ public bool ShowToolTips
}
}

internal override bool SupportsUiaProviders => true;

/// <summary>
/// Returns the number of tabs in the strip
/// </summary>
Expand Down Expand Up @@ -966,6 +968,8 @@ internal void BeginUpdate()
BeginUpdateInternal();
}

protected override AccessibleObject CreateAccessibilityInstance() => new TabControlAccessibleObject(this);

protected override Control.ControlCollection CreateControlsInstance()
{
return new ControlCollection(this);
Expand Down Expand Up @@ -1278,6 +1282,11 @@ protected override void OnGotFocus(EventArgs e)
{
NotifyAboutFocusState(SelectedTab, focused: true);
base.OnGotFocus(e);

if (IsAccessibilityObjectCreated && SelectedTab is not null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference between UIA clients listening and AO create APIs is that the former will force creation of AO if AT (or touch driver) is listening but the object had not been created yet. This might be desired if tab is the first object accessed by narrator. Note that touch driver needs AO created to work as well as AT does.

{
SelectedTab.TabAccessibilityObject.RaiseAutomationEvent(UiaCore.UIA.AutomationFocusChangedEventId);
}
}

/// <summary>
Expand Down Expand Up @@ -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
{
Expand Down
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
{
private TabControl? OwningTabControl => _owningTabPage.ParentInternal as TabControl;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redundant

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)
{
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),
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();
}
}
}
Loading