File: System\Windows\Forms\Controls\PropertyGrid\PropertyGridInternal\GridEntry.GridEntryAccessibleObject.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;
 
namespace System.Windows.Forms.PropertyGridInternal;
 
internal abstract partial class GridEntry
{
    internal class GridEntryAccessibleObject : AccessibleObject, IOwnedObject<GridEntry>
    {
        private readonly WeakReference<GridEntry> _owningGridEntry;
 
        private int[]? _runtimeId;
 
        private delegate void SelectDelegate(AccessibleSelection flags);
 
        public GridEntryAccessibleObject(GridEntry owner)
        {
            Debug.Assert(owner is not null, "GridEntryAccessibleObject must have a valid owner GridEntry");
            _owningGridEntry = new(owner);
        }
 
        private protected override string AutomationId => GetHashCode().ToString();
 
        public override Rectangle Bounds
            => PropertyGridView is not null && PropertyGridView.IsHandleCreated && this.TryGetOwnerAs(out GridEntry? owner)
                ? PropertyGridView.AccessibilityGetGridEntryBounds(owner)
                : Rectangle.Empty;
 
        public override string? DefaultAction
        {
            get
            {
                if (!this.TryGetOwnerAs(out GridEntry? owner) || !owner.Expandable)
                {
                    return base.DefaultAction;
                }
                else if (owner.Expanded)
                {
                    return SR.AccessibleActionCollapse;
                }
                else
                {
                    return SR.AccessibleActionExpand;
                }
            }
        }
 
        internal override bool CanGetDefaultActionInternal => !this.TryGetOwnerAs(out GridEntry? owner) || !owner.Expandable;
 
        public override string? Description
            => this.TryGetOwnerAs(out GridEntry? owner) ? owner.PropertyDescription : string.Empty;
 
        internal override bool CanGetDescriptionInternal => false;
 
        internal override ExpandCollapseState ExpandCollapseState
            => !this.TryGetOwnerAs(out GridEntry? owner) ? ExpandCollapseState.ExpandCollapseState_Collapsed : owner.Expandable
                ? owner.Expanded ? ExpandCollapseState.ExpandCollapseState_Expanded : ExpandCollapseState.ExpandCollapseState_Collapsed
                : ExpandCollapseState.ExpandCollapseState_LeafNode;
 
        public override string? Help => this.TryGetOwnerAs(out GridEntry? owner) ? owner.PropertyDescription : string.Empty;
 
        internal override bool CanGetHelpInternal => false;
 
        public override string? Name => this.TryGetOwnerAs(out GridEntry? owner) ? owner.PropertyLabel : null;
 
        internal override bool CanGetNameInternal => false;
 
        public override AccessibleObject? Parent =>
            this.TryGetOwnerAs(out GridEntry? owner) ? owner.OwnerGridView?.AccessibilityObject : null;
 
        private protected override bool IsInternal => true;
 
        public override AccessibleRole Role => AccessibleRole.Cell;
 
        public override AccessibleStates State
        {
            get
            {
                if (PropertyGridView is null || !PropertyGridView.IsHandleCreated || !this.TryGetOwnerAs(out GridEntry? owner))
                {
                    return AccessibleStates.None;
                }
 
                AccessibleStates state = AccessibleStates.Selectable | AccessibleStates.Focusable;
 
                // Determine focus
                if (owner.HasFocus)
                {
                    state |= AccessibleStates.Focused;
                }
 
                // Determine selected
                Debug.Assert(Parent is not null, "GridEntry AO does not have a parent AO");
                var parent = (PropertyGridView.PropertyGridViewAccessibleObject)Parent;
                if (parent.GetSelected() == this)
                {
                    state |= AccessibleStates.Selected;
                }
 
                // Determine expanded/collapsed state
                if (owner.Expandable)
                {
                    if (owner.Expanded)
                    {
                        state |= AccessibleStates.Expanded;
                    }
                    else
                    {
                        state |= AccessibleStates.Collapsed;
                    }
                }
 
                // Determine readonly/editable state
                if (owner.ShouldRenderReadOnly)
                {
                    state |= AccessibleStates.ReadOnly;
                }
 
                // Determine password state
                if (owner.ShouldRenderPassword)
                {
                    state |= AccessibleStates.Protected;
                }
 
                Rectangle entryBounds = BoundingRectangle;
                Rectangle propertyGridViewBounds = PropertyGridView.GetToolNativeScreenRectangle();
 
                if (!entryBounds.IntersectsWith(propertyGridViewBounds))
                {
                    state |= AccessibleStates.Offscreen;
                }
 
                return state;
            }
        }
 
        public override string? Value
        {
            get => this.TryGetOwnerAs(out GridEntry? owner) ? owner.GetPropertyTextValue() : null;
            set
            {
                if (this.TryGetOwnerAs(out GridEntry? owner))
                {
                    owner.SetPropertyTextValue(value);
                }
            }
        }
 
        internal override bool CanGetValueInternal => false;
 
        internal override bool CanSetValueInternal => false;
 
        internal override int Column => 0;
 
        internal override IRawElementProviderSimple.Interface? ContainingGrid => PropertyGridView?.AccessibilityObject;
 
        internal override IRawElementProviderFragmentRoot.Interface? FragmentRoot
            => Parent as PropertyGridView.PropertyGridViewAccessibleObject;
 
        internal override int Row
        {
            get
            {
                if (Parent is not PropertyGridView.PropertyGridViewAccessibleObject parent
                    || !this.TryGetOwnerAs(out GridEntry? owner))
                {
                    return -1;
                }
 
                if (!parent.TryGetOwnerAs(out PropertyGridView? gridView))
                {
                    return -1;
                }
 
                GridEntryCollection? topLevelGridEntries = gridView.TopLevelGridEntries;
                if (topLevelGridEntries is null)
                {
                    return -1;
                }
 
                for (int i = 0; i < topLevelGridEntries.Count; i++)
                {
                    GridItem? topLevelGridEntry = topLevelGridEntries[i];
                    if (owner == topLevelGridEntry)
                    {
                        return i;
                    }
                }
 
                return -1;
            }
        }
 
        /// <remarks>
        ///  <para>
        ///    For <see cref="GridEntry" /> the item hash code to make the ID unique. Grid items are not controls,
        ///    they don't have windows - we use <see cref="HWND" /> of <see cref="PropertyGridView" />.
        ///  </para>
        /// </remarks>
        /// <inheritdoc cref="AccessibleObject.RuntimeId" />
        internal override int[] RuntimeId => _runtimeId ??=
        [
 
            RuntimeIDFirstItem,
            this.TryGetOwnerAs(out GridEntry? owner)
                ? (int)(owner?.OwnerGridView?.InternalHandle ?? HWND.Null)
                : (int)HWND.Null,
            GetHashCode()
        ];
 
        private PropertyGridView? PropertyGridView
        {
            get
            {
                var propertyGridViewAccessibleObject = Parent as PropertyGridView.PropertyGridViewAccessibleObject;
                if (propertyGridViewAccessibleObject is not null)
                {
                    propertyGridViewAccessibleObject.TryGetOwnerAs(out PropertyGridView? owner);
                    return owner;
                }
 
                return null;
            }
        }
 
        GridEntry? IOwnedObject<GridEntry>.Owner
            => _owningGridEntry.TryGetTarget(out GridEntry? target) ? target : null;
 
        public override void DoDefaultAction()
        {
            if (PropertyGridView is not null && PropertyGridView.IsHandleCreated && this.TryGetOwnerAs(out GridEntry? owner))
            {
                owner.OnOutlineClick(EventArgs.Empty);
            }
        }
 
        /// <summary>
        ///  Returns the currently focused child, if any.
        ///  Returns this if the object itself is focused.
        /// </summary>
        public override AccessibleObject? GetFocused()
            => this.TryGetOwnerAs(out GridEntry? owner) ? owner.HasFocus ? this : null : null;
 
        /// <summary>
        ///  Navigate to the next or previous grid entry.
        /// </summary>
        public override AccessibleObject? Navigate(AccessibleNavigation navdir)
        {
            if (Parent is not PropertyGridView.PropertyGridViewAccessibleObject parent
                || !this.TryGetOwnerAs(out GridEntry? owner))
            {
                return null;
            }
 
            switch (navdir)
            {
                case AccessibleNavigation.Down:
                case AccessibleNavigation.Right:
                case AccessibleNavigation.Next:
                    return parent.Next(owner);
 
                case AccessibleNavigation.Up:
                case AccessibleNavigation.Left:
                case AccessibleNavigation.Previous:
                    return parent.Previous(owner);
 
                case AccessibleNavigation.FirstChild:
                case AccessibleNavigation.LastChild:
                    // Fall through and return null,
                    // as this object has no children.
                    break;
            }
 
            return null;
        }
 
        public override void Select(AccessibleSelection flags)
        {
            if (PropertyGridView is null || !PropertyGridView.IsHandleCreated || !this.TryGetOwnerAs(out GridEntry? owner))
            {
                return;
            }
 
            // Make sure we're on the right thread.
            if (PropertyGridView.InvokeRequired)
            {
                PropertyGridView.Invoke(new SelectDelegate(Select), [flags]);
                return;
            }
 
            // Focus the PropertyGridView window.
            if ((flags & AccessibleSelection.TakeFocus) == AccessibleSelection.TakeFocus)
            {
                bool focused = PropertyGridView.Focus();
            }
 
            // Select the grid entry.
            if ((flags & AccessibleSelection.TakeSelection) == AccessibleSelection.TakeSelection)
            {
                PropertyGridView.AccessibilitySelect(owner);
            }
        }
 
        internal override void Collapse()
        {
            if (this.TryGetOwnerAs(out GridEntry? owner) && owner.Expandable && owner.Expanded)
            {
                owner.Expanded = false;
            }
        }
 
        internal override void Expand()
        {
            if (this.TryGetOwnerAs(out GridEntry? owner) && owner.Expandable && !owner.Expanded)
            {
                owner.Expanded = true;
            }
        }
 
        internal override IRawElementProviderFragment.Interface? FragmentNavigate(NavigateDirection direction)
        {
            switch (direction)
            {
                case NavigateDirection.NavigateDirection_Parent:
                    if (this.TryGetOwnerAs(out GridEntry? owner) && owner.ParentGridEntry is { } parentGridEntry)
                    {
                        return parentGridEntry is SingleSelectRootGridEntry
                            ? owner.OwnerGrid.GridViewAccessibleObject
                            : (IRawElementProviderFragment.Interface)parentGridEntry.AccessibilityObject;
                    }
 
                    return Parent;
                case NavigateDirection.NavigateDirection_PreviousSibling:
                    return Navigate(AccessibleNavigation.Previous);
                case NavigateDirection.NavigateDirection_NextSibling:
                    return Navigate(AccessibleNavigation.Next);
            }
 
            return base.FragmentNavigate(direction);
        }
 
        internal override VARIANT GetPropertyValue(UIA_PROPERTY_ID propertyID)
        {
            return propertyID switch
            {
                // The accessible hierarchy is changed so we cannot use Button type
                // for the grid items to not break automation logic that searches for the first
                // button in the PropertyGridView to show dialog/drop-down. In Level < 3 action
                // button is one of the first children of PropertyGridView.
 
                UIA_PROPERTY_ID.UIA_ControlTypePropertyId => (VARIANT)(int)UIA_CONTROLTYPE_ID.UIA_TreeItemControlTypeId,
                UIA_PROPERTY_ID.UIA_HasKeyboardFocusPropertyId => (VARIANT)(this.TryGetOwnerAs(out GridEntry? owner) && owner.HasFocus),
                UIA_PROPERTY_ID.UIA_IsEnabledPropertyId => VARIANT.True,
                UIA_PROPERTY_ID.UIA_IsKeyboardFocusablePropertyId => (VARIANT)State.HasFlag(AccessibleStates.Focusable),
                _ => base.GetPropertyValue(propertyID)
            };
        }
 
        internal override bool IsIAccessibleExSupported() => this.TryGetOwnerAs(out GridEntry? owner) && owner.Expandable;
 
        internal override bool IsPatternSupported(UIA_PATTERN_ID patternId)
        {
            switch (patternId)
            {
                case UIA_PATTERN_ID.UIA_InvokePatternId:
                case UIA_PATTERN_ID.UIA_LegacyIAccessiblePatternId:
                    return true;
 
                case UIA_PATTERN_ID.UIA_ExpandCollapsePatternId:
                    {
                        if (this.TryGetOwnerAs(out GridEntry? owner) && owner.Expandable)
                        {
                            return true;
                        }
 
                        break;
                    }
 
                case UIA_PATTERN_ID.UIA_GridItemPatternId:
                case UIA_PATTERN_ID.UIA_TableItemPatternId:
                    {
                        if (!this.TryGetOwnerAs(out GridEntry? owner) || owner.OwnerGrid is null || owner.OwnerGrid.SortedByCategories)
                        {
                            break;
                        }
 
                        // Only top level rows are grid items.
                        if (owner.ParentGridEntry is SingleSelectRootGridEntry)
                        {
                            return true;
                        }
 
                        break;
                    }
            }
 
            return base.IsPatternSupported(patternId);
        }
 
        internal override void SetFocus()
        {
            if (PropertyGridView is null || !PropertyGridView.IsHandleCreated)
            {
                return;
            }
 
            base.SetFocus();
 
            RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationFocusChangedEventId);
        }
    }
}