// 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 Windows.Win32.System.Variant; using Windows.Win32.UI.Accessibility; namespace System.Windows.Forms; public partial class ListViewItem { /// <summary> /// This class contains the base implementation of properties and methods for ListViewItem accessibility objects. /// </summary> /// <remarks> /// <para> /// The implementation of this class fully corresponds to the behavior of the ListViewItem accessibility /// object when the ListView is in "LargeIcon" or "SmallIcon" view. /// </para> /// </remarks> internal abstract class ListViewItemBaseAccessibleObject : AccessibleObject { private protected readonly ListView _owningListView; private protected readonly ListViewItem _owningItem; public ListViewItemBaseAccessibleObject(ListViewItem owningItem) { _owningItem = owningItem.OrThrowIfNull(); _owningListView = owningItem.ListView ?? owningItem.Group?.ListView ?? throw new InvalidOperationException(nameof(owningItem.ListView)); } private protected ListViewGroup? OwningGroup => _owningListView.GroupsDisplayed ? _owningItem.Group ?? _owningListView.DefaultGroup : null; private protected override string AutomationId => $"{nameof(ListViewItem)}-{CurrentIndex}"; public override Rectangle Bounds => !_owningListView.IsHandleCreated || OwningGroup?.CollapsedState == ListViewGroupCollapsedState.Collapsed ? Rectangle.Empty : new Rectangle( _owningListView.AccessibilityObject.Bounds.X + _owningItem.Bounds.X, _owningListView.AccessibilityObject.Bounds.Y + _owningItem.Bounds.Y, _owningItem.Bounds.Width, _owningItem.Bounds.Height); internal int CurrentIndex => _owningItem.Index; internal virtual int FirstSubItemIndex => 0; internal override IRawElementProviderFragmentRoot.Interface FragmentRoot => _owningListView.AccessibilityObject; internal bool HasImage => _owningItem.ImageList is not null && _owningItem.ImageList.Images.Count > 0 && _owningItem.ImageIndex != ImageList.Indexer.DefaultIndex; internal override bool IsItemSelected => (State & AccessibleStates.Selected) != 0; public override string? Name => _owningItem.Text; internal override bool CanGetNameInternal => false; private bool OwningListItemFocused { get { bool owningListViewFocused = _owningListView.Focused; bool owningListItemFocused = _owningListView.FocusedItem == _owningItem; return owningListViewFocused && owningListItemFocused; } } public override AccessibleRole Role => _owningListView.CheckBoxes ? AccessibleRole.CheckButton : AccessibleRole.ListItem; public override AccessibleStates State { get { AccessibleStates state = AccessibleStates.Selectable | AccessibleStates.Focusable | AccessibleStates.MultiSelectable; if (_owningListView.SelectedIndices.Contains(_owningItem.Index)) { return state |= AccessibleStates.Selected | AccessibleStates.Focused; } return state |= _owningListView.AccessibilityObject.SystemIAccessible.TryGetState(GetChildId()); } } protected abstract View View { get; } internal override void AddToSelection() => SelectItem(); public override string DefaultAction { get { if (_owningListView.CheckBoxes) { return _owningItem.Checked ? SR.AccessibleActionUncheck : SR.AccessibleActionCheck; } return SR.AccessibleActionDoubleClick; } } private protected override bool IsInternal => true; internal override bool CanGetDefaultActionInternal => false; public override void DoDefaultAction() { if (_owningListView.CheckBoxes) { Toggle(); } SetFocus(); } internal override IRawElementProviderFragment.Interface? FragmentNavigate(NavigateDirection direction) { AccessibleObject parentInternal = OwningGroup?.AccessibilityObject ?? _owningListView.AccessibilityObject; switch (direction) { case NavigateDirection.NavigateDirection_Parent: return parentInternal; case NavigateDirection.NavigateDirection_NextSibling: int childIndex = parentInternal.GetChildIndex(this); return childIndex == InvalidIndex ? null : parentInternal.GetChild(childIndex + 1); case NavigateDirection.NavigateDirection_PreviousSibling: return parentInternal.GetChild(parentInternal.GetChildIndex(this) - 1); case NavigateDirection.NavigateDirection_FirstChild: case NavigateDirection.NavigateDirection_LastChild: return null; } return base.FragmentNavigate(direction); } public override AccessibleObject? GetChild(int index) { if (_owningListView.View != View) { throw new InvalidOperationException(string.Format(SR.ListViewItemAccessibilityObjectInvalidViewException, View.ToString())); } return null; } internal virtual AccessibleObject? GetChildInternal(int index) => GetChild(index); public override int GetChildCount() { if (_owningListView.View != View) { throw new InvalidOperationException(string.Format(SR.ListViewItemAccessibilityObjectInvalidViewException, View.ToString())); } return InvalidIndex; } internal override int GetChildIndex(AccessibleObject? child) => InvalidIndex; internal override VARIANT GetPropertyValue(UIA_PROPERTY_ID propertyID) { switch (propertyID) { case UIA_PROPERTY_ID.UIA_ControlTypePropertyId: return (VARIANT)(int)UIA_CONTROLTYPE_ID.UIA_ListItemControlTypeId; case UIA_PROPERTY_ID.UIA_HasKeyboardFocusPropertyId: return (VARIANT)OwningListItemFocused; case UIA_PROPERTY_ID.UIA_IsEnabledPropertyId: return (VARIANT)_owningListView.Enabled; case UIA_PROPERTY_ID.UIA_IsKeyboardFocusablePropertyId: return (VARIANT)State.HasFlag(AccessibleStates.Focusable); case UIA_PROPERTY_ID.UIA_IsOffscreenPropertyId: if (OwningGroup?.CollapsedState == ListViewGroupCollapsedState.Collapsed) { return VARIANT.True; } VARIANT result = base.GetPropertyValue(UIA_PROPERTY_ID.UIA_IsOffscreenPropertyId); return result.IsEmpty ? VARIANT.False : result; case UIA_PROPERTY_ID.UIA_NativeWindowHandlePropertyId: return UIAHelper.WindowHandleToVariant(HWND.Null); default: return base.GetPropertyValue(propertyID); } } internal virtual Rectangle GetSubItemBounds(int index) => Rectangle.Empty; internal override int[] RuntimeId { get { int[] id = _owningListView.AccessibilityObject.RuntimeId; Debug.Assert(id.Length >= 2); return [ id[0], id[1], // Win32-control specific RuntimeID constant. 4, // RuntimeId uses hash code instead of item's index. When items are removed, // indexes of below items shift. But when UiaDisconnectProvider is called for item // with updated index, it in fact disconnects the item which had the index initially, // apparently because of lack of synchronization with RuntimeId updates. // Similar applies for items within a group, where adding the group's index // was preventing from correct disconnection of items on removal. _owningItem.GetHashCode() ]; } } internal override ToggleState ToggleState => _owningItem.Checked ? ToggleState.ToggleState_On : ToggleState.ToggleState_Off; /// <summary> /// Indicates whether specified pattern is supported. /// </summary> /// <param name="patternId">The pattern ID.</param> /// <returns>True if specified </returns> internal override bool IsPatternSupported(UIA_PATTERN_ID patternId) => patternId switch { UIA_PATTERN_ID.UIA_ScrollItemPatternId => true, UIA_PATTERN_ID.UIA_LegacyIAccessiblePatternId => true, UIA_PATTERN_ID.UIA_SelectionItemPatternId => true, UIA_PATTERN_ID.UIA_InvokePatternId => true, UIA_PATTERN_ID.UIA_TogglePatternId => _owningListView.CheckBoxes, _ => base.IsPatternSupported(patternId) }; internal virtual void ReleaseChildUiaProviders() { foreach (ListViewSubItem subItem in _owningItem.SubItems) { subItem.ReleaseUiaProvider(); } } internal override void RemoveFromSelection() { // Do nothing, C++ implementation returns UIA_E_INVALIDOPERATION 0x80131509 } internal override IRawElementProviderSimple.Interface ItemSelectionContainer => _owningListView.AccessibilityObject; internal override void ScrollIntoView() => _owningItem.EnsureVisible(); internal override unsafe void SelectItem() { if (_owningListView.IsHandleCreated) { _owningListView.SelectedIndices.Add(CurrentIndex); PInvoke.InvalidateRect(_owningListView, lpRect: null, bErase: false); } RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationFocusChangedEventId); RaiseAutomationEvent(UIA_EVENT_ID.UIA_SelectionItem_ElementSelectedEventId); } internal override void SetFocus() { RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationFocusChangedEventId); SelectItem(); } public override void Select(AccessibleSelection flags) { if (!_owningListView.IsHandleCreated) { return; } _owningListView.AccessibilityObject.SystemIAccessible.TrySelect(flags, GetChildId()); // In Everett, the ListBox accessible children did not have any selection capability. // In Whidbey, they delegate the selection capability to OLEACC. // However, OLEACC does not deal w/ several Selection flags: ExtendSelection, AddSelection, RemoveSelection. // OLEACC instead throws an ArgumentException. // Since Whidbey API's should not throw an exception in places where Everett API's did not, we catch // the ArgumentException and fail silently. } internal override void Toggle() => _owningItem.Checked = !_owningItem.Checked; } } |