// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Drawing; using System.Globalization; using Windows.Win32.System.Variant; using Windows.Win32.UI.Accessibility; using PInvoke = Windows.Win32.PInvoke; namespace System.Windows.Forms; public abstract partial class ToolStripItem { /// <summary> /// An implementation of AccessibleChild for use with ToolStripItems /// </summary> public class ToolStripItemAccessibleObject : AccessibleObject { private readonly ToolStripItem _ownerItem; // The associated ToolStripItem for this AccessibleChild (if any) private AccessibleStates _additionalState = AccessibleStates.None; // Test hook for the designer private int[]? _runtimeId; public ToolStripItemAccessibleObject(ToolStripItem ownerItem) { _ownerItem = ownerItem.OrThrowIfNull(); } public override string DefaultAction { get { string? defaultAction = _ownerItem.AccessibleDefaultActionDescription; if (defaultAction is not null) { return defaultAction; } return SR.AccessibleActionPress; } } internal override bool CanGetDefaultActionInternal => false; public override string? Description => _ownerItem.AccessibleDescription is { } description ? description : base.Description; internal override bool CanGetDescriptionInternal => IsInternal && _ownerItem.AccessibleDescription is null; public override string? Help { get { QueryAccessibilityHelpEventHandler? handler = (QueryAccessibilityHelpEventHandler?)Owner.Events[s_queryAccessibilityHelpEvent]; if (handler is not null) { QueryAccessibilityHelpEventArgs args = new(); handler(Owner, args); return args.HelpString; } return base.Help; } } internal override bool CanGetHelpInternal => IsInternal && (QueryAccessibilityHelpEventHandler?)Owner.Events[s_queryAccessibilityHelpEvent] is null; public override string KeyboardShortcut { get { // This really is the Mnemonic - NOT the shortcut. E.g. in notepad Edit->Replace is Control+H // but the KeyboardShortcut comes up as the mnemonic 'r'. char mnemonic = WindowsFormsUtils.GetMnemonic(_ownerItem.Text, false); if (_ownerItem.IsOnDropDown) { // no ALT on dropdown return mnemonic == '\0' ? string.Empty : mnemonic.ToString(); } return mnemonic == '\0' ? string.Empty : $"Alt+{mnemonic}"; } } internal override bool CanGetKeyboardShortcutInternal => false; /// <remarks> /// <para> /// First item should be <see cref="PInvoke.UiaAppendRuntimeId" /> since this is not a top-level /// element of the fragment. Second item can be anything, but here it is the owner hash code. /// For <see cref="ToolStrip" /> hash code is unique even with child controls. /// <see cref="Control.Handle" /> is not. /// </para> /// </remarks> /// <inheritdoc cref="AccessibleObject.RuntimeId" /> internal override int[] RuntimeId => _runtimeId ??= [ (int)PInvoke.UiaAppendRuntimeId, _ownerItem.GetHashCode() ]; internal override VARIANT GetPropertyValue(UIA_PROPERTY_ID propertyID) => propertyID switch { // "ControlType" value depends on owner's AccessibleRole value. // See: docs/accessibility/accessible-role-controltype.md UIA_PROPERTY_ID.UIA_ControlTypePropertyId => (VARIANT)(int)AccessibleRoleControlTypeMap.GetControlType(Role), UIA_PROPERTY_ID.UIA_HasKeyboardFocusPropertyId => (VARIANT)_ownerItem.Selected, UIA_PROPERTY_ID.UIA_IsEnabledPropertyId => (VARIANT)_ownerItem.Enabled, UIA_PROPERTY_ID.UIA_IsKeyboardFocusablePropertyId => (VARIANT)_ownerItem.CanSelect, UIA_PROPERTY_ID.UIA_IsOffscreenPropertyId => (VARIANT)GetIsOffscreenPropertyValue(_ownerItem.Placement, Bounds), UIA_PROPERTY_ID.UIA_IsControlElementPropertyId => VARIANT.True, UIA_PROPERTY_ID.UIA_IsContentElementPropertyId => VARIANT.True, _ => base.GetPropertyValue(propertyID) }; public override string? Name { get { string? name = _ownerItem.AccessibleName; if (name is not null) { return name; } string? baseName = base.Name; if (string.IsNullOrEmpty(baseName)) { return WindowsFormsUtils.TextWithoutMnemonics(_ownerItem.Text)!; } return baseName; } set => _ownerItem.AccessibleName = value; } internal override bool CanGetNameInternal => false; internal override bool CanSetNameInternal => false; internal ToolStripItem Owner => _ownerItem; public override AccessibleRole Role { get { AccessibleRole role = _ownerItem.AccessibleRole; if (role != AccessibleRole.Default) { return role; } return AccessibleRole.PushButton; } } public override AccessibleStates State { get { if (!_ownerItem.CanSelect) { return base.State | _additionalState; } if (!_ownerItem.Enabled) { if (_ownerItem.Selected && _ownerItem is ToolStripMenuItem) { return AccessibleStates.Unavailable | _additionalState | AccessibleStates.Focused; } // Disabled menu items that are selected must have focus // state so that Narrator can announce them. if (_ownerItem.Selected && _ownerItem is ToolStripMenuItem) { return AccessibleStates.Focused; } return AccessibleStates.Unavailable | _additionalState; } AccessibleStates accState = AccessibleStates.Focusable | _additionalState; if (_ownerItem.Selected || _ownerItem.Pressed) { accState |= AccessibleStates.Focused | AccessibleStates.HotTracked; } if (_ownerItem.Pressed) { accState |= AccessibleStates.Pressed; } return accState; } } public override void DoDefaultAction() { Owner?.PerformClick(); } public override int GetHelpTopic(out string? fileName) { int topic = 0; QueryAccessibilityHelpEventHandler? handler = (QueryAccessibilityHelpEventHandler?)Owner.Events[s_queryAccessibilityHelpEvent]; if (handler is not null) { QueryAccessibilityHelpEventArgs args = new(); handler(Owner, args); fileName = args.HelpNamespace; int.TryParse(args.HelpKeyword, NumberStyles.Integer, CultureInfo.InvariantCulture, out topic); return topic; } return base.GetHelpTopic(out fileName); } internal override bool CanGetHelpTopicInternal => IsInternal && Owner.Events[s_queryAccessibilityHelpEvent] is null; public override AccessibleObject? Navigate(AccessibleNavigation navigationDirection) { ToolStripItem? nextItem = null; if (Owner is not null) { ToolStrip? parent = Owner.ParentInternal; if (parent is null) { return null; } switch (navigationDirection) { case AccessibleNavigation.FirstChild: nextItem = parent.GetNextItem(null, ArrowDirection.Right, /*RTLAware=*/true); break; case AccessibleNavigation.LastChild: nextItem = parent.GetNextItem(null, ArrowDirection.Left, /*RTLAware=*/true); break; case AccessibleNavigation.Previous: case AccessibleNavigation.Left: nextItem = parent.GetNextItem(Owner, ArrowDirection.Left, /*RTLAware=*/true); break; case AccessibleNavigation.Next: case AccessibleNavigation.Right: nextItem = parent.GetNextItem(Owner, ArrowDirection.Right, /*RTLAware=*/true); break; case AccessibleNavigation.Up: nextItem = (Owner.IsOnDropDown) ? parent.GetNextItem(Owner, ArrowDirection.Up) : parent.GetNextItem(Owner, ArrowDirection.Left, /*RTLAware=*/true); break; case AccessibleNavigation.Down: nextItem = (Owner.IsOnDropDown) ? parent.GetNextItem(Owner, ArrowDirection.Down) : parent.GetNextItem(Owner, ArrowDirection.Right, /*RTLAware=*/true); break; } } return nextItem?.AccessibilityObject; } public void AddState(AccessibleStates state) { if (state == AccessibleStates.None) { _additionalState = state; } else { _additionalState |= state; } } public override string ToString() { if (Owner is not null) { return $"ToolStripItemAccessibleObject: Owner = {Owner}"; } return "ToolStripItemAccessibleObject: Owner = null"; } /// <summary> /// Gets the bounds of the accessible object, in screen coordinates. /// </summary> public override Rectangle Bounds { get { Rectangle bounds = Owner.Bounds; if (Owner.ParentInternal is not null && Owner.ParentInternal.Visible) { return new Rectangle(Owner.ParentInternal.PointToScreen(bounds.Location), bounds.Size); } return Rectangle.Empty; } } /// <summary> /// When overridden in a derived class, gets or sets the parent of an accessible object. /// </summary> public override AccessibleObject? Parent { get { if (Owner.IsOnDropDown) { // Return the owner item as the accessible parent. ToolStripDropDown dropDown = Owner.GetCurrentParentDropDown()!; return dropDown.AccessibilityObject; } return (Owner.Parent is not null) ? Owner.Parent.AccessibilityObject : base.Parent; } } internal override IRawElementProviderFragmentRoot.Interface? FragmentRoot => _ownerItem.RootToolStrip?.AccessibilityObject; internal override IRawElementProviderFragment.Interface? FragmentNavigate(NavigateDirection direction) { switch (direction) { case NavigateDirection.NavigateDirection_Parent: return Parent; case NavigateDirection.NavigateDirection_NextSibling: case NavigateDirection.NavigateDirection_PreviousSibling: int index = GetChildFragmentIndex(); if (index == -1) { Debug.Fail("No item matched the index?"); return null; } AccessibleObject? sibling = null; index += direction == NavigateDirection.NavigateDirection_NextSibling ? 1 : -1; int itemsCount = GetChildFragmentCount(); if (index >= 0 && index < itemsCount) { sibling = GetChildFragment(index, direction); } return sibling; } return base.FragmentNavigate(direction); } private AccessibleObject? GetChildFragment(int index, NavigateDirection direction) { if (Parent is ToolStrip.ToolStripAccessibleObject toolStripParent) { return toolStripParent.GetChildFragment(index, direction); } // ToolStripOverflowButtonAccessibleObject is derived from ToolStripDropDownItemAccessibleObject // and we should not process ToolStripOverflowButton as a ToolStripDropDownItem here so check for // the ToolStripOverflowButton firstly as more specific condition. if (Parent is ToolStripOverflowButton.ToolStripOverflowButtonAccessibleObject toolStripOverflowButtonParent) { if (toolStripOverflowButtonParent.Parent is ToolStrip.ToolStripAccessibleObject toolStripGrandParent) { return toolStripGrandParent.GetChildFragment(index, direction, true); } } if (Parent is ToolStripDropDownItemAccessibleObject dropDownItemParent) { return dropDownItemParent.GetChildFragment(index, direction); } return null; } private int GetChildFragmentCount() { if (Parent is ToolStrip.ToolStripAccessibleObject toolStripParent) { return toolStripParent.GetChildFragmentCount(); } if (Parent is ToolStripOverflowButton.ToolStripOverflowButtonAccessibleObject toolStripOverflowButtonParent) { if (toolStripOverflowButtonParent.Parent is ToolStrip.ToolStripAccessibleObject toolStripGrandParent) { return toolStripGrandParent.GetChildOverflowFragmentCount(); } } if (Parent is ToolStripDropDownItemAccessibleObject dropDownItemParent) { return dropDownItemParent.GetChildCount(); } return -1; } private int GetChildFragmentIndex() { if (Parent is ToolStrip.ToolStripAccessibleObject toolStripParent) { return toolStripParent.GetChildFragmentIndex(this); } if (Parent is ToolStripOverflowButton.ToolStripOverflowButtonAccessibleObject toolStripOverflowButtonParent) { if (toolStripOverflowButtonParent.Parent is ToolStrip.ToolStripAccessibleObject toolStripGrandParent) { return toolStripGrandParent.GetChildFragmentIndex(this); } } if (Parent is ToolStripDropDownItemAccessibleObject dropDownItemParent) { return dropDownItemParent.GetChildFragmentIndex(this); } return -1; } internal override bool IsPatternSupported(UIA_PATTERN_ID patternId) { if (patternId == UIA_PATTERN_ID.UIA_LegacyIAccessiblePatternId) { return true; } return base.IsPatternSupported(patternId); } internal override void SetFocus() => Owner.Select(); internal void RaiseFocusChanged() { ToolStrip? root = _ownerItem.RootToolStrip; if (root is not null && root.IsHandleCreated && root.SupportsUiaProviders) { RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationFocusChangedEventId); } } } } |