File: System\Windows\Forms\Controls\ListView\ListViewGroup.ListViewGroupAccessibleObject.cs
Web Access
Project: src\src\System.Windows.Forms\src\System.Windows.Forms.csproj (System.Windows.Forms)
// 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;
using static System.Windows.Forms.ListView;
 
namespace System.Windows.Forms;
 
public partial class ListViewGroup
{
    internal sealed class ListViewGroupAccessibleObject : AccessibleObject
    {
        private readonly ListView _owningListView;
        private readonly ListViewAccessibleObject _owningListViewAccessibilityObject;
        private readonly ListViewGroup _owningGroup;
        private readonly bool _owningGroupIsDefault;
 
        public ListViewGroupAccessibleObject(ListViewGroup owningGroup, bool owningGroupIsDefault)
        {
            _owningGroup = owningGroup.OrThrowIfNull();
 
            // Using item from group for getting of ListView is a workaround for https://github.com/dotnet/winforms/issues/4019
            _owningListView = owningGroup.ListView
                ?? (owningGroup.Items.Count > 0 && _owningGroup.Items[0].ListView is ListView listView
                    ? listView
                    : throw new InvalidOperationException(nameof(owningGroup.ListView)));
 
            _owningListViewAccessibilityObject = _owningListView.AccessibilityObject as ListViewAccessibleObject
                ?? throw new InvalidOperationException(nameof(_owningListView.AccessibilityObject));
 
            _owningGroupIsDefault = owningGroupIsDefault;
        }
 
        private protected override string AutomationId
            => $"{nameof(ListViewGroup)}-{CurrentIndex}";
 
        public override Rectangle Bounds
        {
            get
            {
                if (!_owningListView.IsHandleCreated || !_owningListView.GroupsDisplayed || IsEmpty)
                {
                    return Rectangle.Empty;
                }
 
                int nativeGroupId = GetNativeGroupId();
                if (nativeGroupId == -1)
                {
                    return Rectangle.Empty;
                }
 
                uint rectType = _owningGroup.CollapsedState == ListViewGroupCollapsedState.Collapsed
                    ? PInvoke.LVGGR_HEADER
                    : PInvoke.LVGGR_GROUP;
 
                // Get the native rectangle
                RECT groupRect = default;
 
                // Using the "top" property, we set which rectangle type of the group we want to get
                // This is described in more detail in https://docs.microsoft.com/windows/win32/controls/lvm-getgrouprect
                groupRect.top = (int)rectType;
                PInvokeCore.SendMessage(_owningListView, PInvoke.LVM_GETGROUPRECT, (WPARAM)nativeGroupId, ref groupRect);
 
                // Using the following code, we limit the size of the ListViewGroup rectangle
                // so that it does not go beyond the rectangle of the ListView
                Rectangle listViewBounds = _owningListView.AccessibilityObject.Bounds;
                groupRect = _owningListView.RectangleToScreen(groupRect);
                groupRect.top = Math.Max(listViewBounds.Top, groupRect.top);
                groupRect.bottom = Math.Min(listViewBounds.Bottom, groupRect.bottom);
                groupRect.left = Math.Max(listViewBounds.Left, groupRect.left);
                groupRect.right = Math.Min(listViewBounds.Right, groupRect.right);
 
                return groupRect;
            }
        }
 
        internal int CurrentIndex =>
            // The default group has 0 index, as it is always displayed first.
            _owningGroupIsDefault
                ? 0
                // When calculating the index of other groups, we add a shift if the default group is displayed
                : _owningListViewAccessibilityObject.OwnerHasDefaultGroup
                    ? _owningListView.Groups.IndexOf(_owningGroup) + 1
                    : _owningListView.Groups.IndexOf(_owningGroup);
 
        public override string DefaultAction => SR.AccessibleActionDoubleClick;
 
        private protected override bool IsInternal => true;
 
        internal override bool CanGetDefaultActionInternal => false;
 
        internal override ExpandCollapseState ExpandCollapseState =>
            _owningGroup.CollapsedState == ListViewGroupCollapsedState.Collapsed
                ? ExpandCollapseState.ExpandCollapseState_Collapsed
                : ExpandCollapseState.ExpandCollapseState_Expanded;
 
        internal override IRawElementProviderFragmentRoot.Interface FragmentRoot => _owningListView.AccessibilityObject;
 
        public override string Name =>
            !string.IsNullOrEmpty(_owningGroup.Subtitle)
                ? $"{_owningGroup.Header}. {_owningGroup.Subtitle}"
                : _owningGroup.Header;
 
        internal override bool CanGetNameInternal => false;
 
        public override AccessibleRole Role => AccessibleRole.Grouping;
 
        internal override int[] RuntimeId
        {
            get
            {
                int[] id = _owningListViewAccessibilityObject.RuntimeId;
 
                Debug.Assert(id.Length >= 2);
 
                return
                [
                    id[0],
                    id[1],
                    // Win32 control specific RuntimeID constant, is used in similar Win32 controls and is used in WinForms controls for consistency.
                    4,
                    GetHashCode()
                ];
            }
        }
 
        public override AccessibleStates State
        {
            get
            {
                AccessibleStates state = AccessibleStates.Focusable;
 
                if (Focused)
                {
                    state |= AccessibleStates.Focused;
                }
 
                return state;
            }
        }
 
        private bool IsEmpty => GetVisibleItems().Count == 0;
 
        internal override void Collapse()
            => _owningGroup.CollapsedState = ListViewGroupCollapsedState.Collapsed;
 
        public override void DoDefaultAction() => SetFocus();
 
        internal override void Expand()
            => _owningGroup.CollapsedState = ListViewGroupCollapsedState.Expanded;
 
        private bool Focused
            => GetNativeFocus() || _owningGroup.Focused;
 
        private bool GetNativeFocus()
        {
            if (!_owningListView.IsHandleCreated || !_owningListView.GroupsDisplayed)
            {
                return false;
            }
 
            int nativeGroupId = GetNativeGroupId();
            if (nativeGroupId == -1)
            {
                return false;
            }
 
            return (LIST_VIEW_GROUP_STATE_FLAGS)(uint)PInvokeCore.SendMessage(
                _owningListView,
                PInvoke.LVM_GETGROUPSTATE,
                (WPARAM)nativeGroupId,
                (LPARAM)(uint)LIST_VIEW_GROUP_STATE_FLAGS.LVGS_FOCUSED) == LIST_VIEW_GROUP_STATE_FLAGS.LVGS_FOCUSED;
        }
 
        private int GetNativeGroupId()
        {
            if (PInvokeCore.SendMessage(_owningListView, PInvoke.LVM_HASGROUP, (WPARAM)_owningGroup.ID) == 0)
            {
                return -1;
            }
 
            return _owningGroup.ID;
        }
 
        internal override VARIANT GetPropertyValue(UIA_PROPERTY_ID propertyID)
            => propertyID switch
            {
                UIA_PROPERTY_ID.UIA_ControlTypePropertyId => (VARIANT)(int)UIA_CONTROLTYPE_ID.UIA_GroupControlTypeId,
                UIA_PROPERTY_ID.UIA_HasKeyboardFocusPropertyId => (VARIANT)(_owningListView.Focused && Focused),
                UIA_PROPERTY_ID.UIA_IsEnabledPropertyId => (VARIANT)_owningListView.Enabled,
                UIA_PROPERTY_ID.UIA_IsKeyboardFocusablePropertyId => (VARIANT)State.HasFlag(AccessibleStates.Focusable),
                UIA_PROPERTY_ID.UIA_NativeWindowHandlePropertyId => UIAHelper.WindowHandleToVariant(HWND.Null),
                _ => base.GetPropertyValue(propertyID)
            };
 
        internal IReadOnlyList<ListViewItem> GetVisibleItems()
        {
            List<ListViewItem> visibleItems = [];
            if (_owningGroupIsDefault)
            {
                foreach (ListViewItem? listViewItem in _owningListView.Items)
                {
                    if (listViewItem is not null && listViewItem.Group is null)
                    {
                        visibleItems.Add(listViewItem);
                    }
                }
 
                return visibleItems;
            }
 
            foreach (ListViewItem listViewItem in _owningGroup.Items)
            {
                if (listViewItem.ListView is not null)
                {
                    visibleItems.Add(listViewItem);
                }
            }
 
            return visibleItems;
        }
 
        internal override IRawElementProviderFragment.Interface? FragmentNavigate(NavigateDirection direction)
        {
            if (!_owningListView.IsHandleCreated || !_owningListView.GroupsDisplayed || IsEmpty)
            {
                return null;
            }
 
            switch (direction)
            {
                case NavigateDirection.NavigateDirection_Parent:
                    return _owningListViewAccessibilityObject;
                case NavigateDirection.NavigateDirection_NextSibling:
                    int childIndex = _owningListViewAccessibilityObject.GetChildIndex(this);
                    return childIndex == InvalidIndex
                        ? null
                        : _owningListViewAccessibilityObject.GetChild(childIndex + 1);
                case NavigateDirection.NavigateDirection_PreviousSibling:
                    return _owningListViewAccessibilityObject.GetChild(_owningListViewAccessibilityObject.GetChildIndex(this) - 1);
                case NavigateDirection.NavigateDirection_FirstChild:
                    return GetChild(0);
                case NavigateDirection.NavigateDirection_LastChild:
                    IReadOnlyList<ListViewItem> visibleItems = GetVisibleItems();
                    return visibleItems.Count > 0 ? visibleItems[visibleItems.Count - 1].AccessibilityObject : null;
 
                default:
                    return null;
            }
        }
 
        public override AccessibleObject? GetChild(int index)
        {
            if (!_owningListView.IsHandleCreated || !_owningListView.GroupsDisplayed || index < 0)
            {
                return null;
            }
 
            IReadOnlyList<ListViewItem> visibleItems = GetVisibleItems();
            if (index >= visibleItems.Count)
            {
                return null;
            }
 
            return visibleItems[index].AccessibilityObject;
        }
 
        internal override int GetChildIndex(AccessibleObject? child)
        {
            if (child is null || !_owningListView.IsHandleCreated || !_owningListView.GroupsDisplayed)
            {
                return InvalidIndex;
            }
 
            IReadOnlyList<ListViewItem> visibleItems = GetVisibleItems();
            for (int i = 0; i < visibleItems.Count; i++)
            {
                if (visibleItems[i].AccessibilityObject == child)
                {
                    return i;
                }
            }
 
            return InvalidIndex;
        }
 
        public override int GetChildCount()
        {
            if (!_owningListView.IsHandleCreated || !_owningListView.GroupsDisplayed)
            {
                return InvalidIndex;
            }
 
            return GetVisibleItems().Count;
        }
 
        internal override bool IsPatternSupported(UIA_PATTERN_ID patternId)
            => patternId switch
            {
                UIA_PATTERN_ID.UIA_LegacyIAccessiblePatternId => true,
                UIA_PATTERN_ID.UIA_ExpandCollapsePatternId => _owningGroup.CollapsedState != ListViewGroupCollapsedState.Default,
                _ => base.IsPatternSupported(patternId),
            };
 
        internal override unsafe void SetFocus()
        {
            if (!_owningListView.IsHandleCreated || !_owningListView.GroupsDisplayed || IsEmpty)
            {
                return;
            }
 
            _owningListView.FocusedGroup = _owningGroup;
 
            RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationFocusChangedEventId);
        }
    }
}