File: System\Windows\Forms\Controls\DataGridView\DataGridViewCell.DataGridViewCellAccessibleObject.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.ComponentModel;
using System.Drawing;
using System.Windows.Forms.Primitives;
using Windows.Win32.System.Com;
using Windows.Win32.System.Variant;
using Windows.Win32.UI.Accessibility;
 
namespace System.Windows.Forms;
 
public abstract partial class DataGridViewCell
{
    protected class DataGridViewCellAccessibleObject : AccessibleObject
    {
        private int[] _runtimeId = null!; // Used by UIAutomation
        private AccessibleObject? _child;
        private DataGridViewCell? _owner;
 
        public DataGridViewCellAccessibleObject()
        {
        }
 
        public DataGridViewCellAccessibleObject(DataGridViewCell? owner)
        {
            _owner = owner;
        }
 
        public override Rectangle Bounds => GetAccessibleObjectBounds(GetAccessibleObjectParent());
 
        public override string DefaultAction
        {
            get
            {
                if (Owner is null)
                {
                    throw new InvalidOperationException(SR.DataGridViewCellAccessibleObject_OwnerNotSet);
                }
 
                return !Owner.ReadOnly ? SR.DataGridView_AccCellDefaultAction : string.Empty;
            }
        }
 
        internal override bool CanGetDefaultActionInternal => false;
 
        public override string? Name
        {
            get
            {
                if (_owner is null)
                {
                    throw new InvalidOperationException(SR.DataGridViewCellAccessibleObject_OwnerNotSet);
                }
 
                if (_owner.OwningColumn is null || _owner.OwningRow is null)
                {
                    return string.Empty;
                }
 
                int rowIndex = _owner.DataGridView is null
                    ? -1
                    : _owner.DataGridView.Rows.GetVisibleIndex(_owner.OwningRow) + RowStartIndex;
 
                string name = string.Format(SR.DataGridView_AccDataGridViewCellName, _owner.OwningColumn.HeaderText, rowIndex).Trim();
 
                if (_owner.OwningColumn.SortMode != DataGridViewColumnSortMode.NotSortable)
                {
                    DataGridViewCell dataGridViewCell = _owner;
                    DataGridView? dataGridView = dataGridViewCell.DataGridView;
 
                    if (dataGridView is not null &&
                        dataGridViewCell.OwningColumn is not null &&
                        dataGridViewCell.OwningColumn == dataGridView.SortedColumn)
                    {
                        name += ", " + (dataGridView.SortOrder == SortOrder.Ascending
                            ? SR.SortedAscendingAccessibleStatus
                            : SR.SortedDescendingAccessibleStatus);
                    }
                    else
                    {
                        name += $", {SR.NotSortedAccessibleStatus}";
                    }
                }
 
                return name;
            }
        }
 
        internal override bool CanGetNameInternal => false;
 
        public DataGridViewCell? Owner
        {
            get => _owner;
            set
            {
                if (_owner is not null)
                {
                    throw new InvalidOperationException(SR.DataGridViewCellAccessibleObject_OwnerAlreadySet);
                }
 
                _owner = value;
            }
        }
 
        public override AccessibleObject? Parent => ParentPrivate;
 
        private AccessibleObject? ParentPrivate
        {
            get
            {
                if (_owner is null)
                {
                    throw new InvalidOperationException(SR.DataGridViewCellAccessibleObject_OwnerNotSet);
                }
 
                return _owner.OwningRow?.AccessibilityObject;
            }
        }
 
        public override AccessibleRole Role => AccessibleRole.Cell;
 
        private static int RowStartIndex => LocalAppContextSwitches.DataGridViewUIAStartRowCountAtZero ? 0 : 1;
 
        public override AccessibleStates State
        {
            get
            {
                if (_owner is null)
                {
                    throw new InvalidOperationException(SR.DataGridViewCellAccessibleObject_OwnerNotSet);
                }
 
                AccessibleStates state = AccessibleStates.Selectable | AccessibleStates.Focusable;
                if (_owner.DataGridView is not null && _owner == _owner.DataGridView.CurrentCell)
                {
                    state |= AccessibleStates.Focused;
                }
 
                if (_owner.Selected)
                {
                    state |= AccessibleStates.Selected;
                }
 
                if (_owner.ReadOnly)
                {
                    state |= AccessibleStates.ReadOnly;
                }
 
                if (_owner.DataGridView?.IsHandleCreated != true)
                {
                    return state;
                }
 
                Rectangle cellBounds;
                if (_owner.OwningColumn is not null && _owner.OwningRow is not null)
                {
                    cellBounds = _owner.DataGridView.GetCellDisplayRectangle(_owner.OwningColumn.Index, _owner.OwningRow.Index, cutOverflow: false);
                }
                else if (_owner.OwningRow is not null)
                {
                    cellBounds = _owner.DataGridView.GetCellDisplayRectangle(-1, _owner.OwningRow.Index, cutOverflow: false);
                }
                else if (_owner.OwningColumn is not null)
                {
                    cellBounds = _owner.DataGridView.GetCellDisplayRectangle(_owner.OwningColumn.Index, -1, cutOverflow: false);
                }
                else
                {
                    cellBounds = _owner.DataGridView.GetCellDisplayRectangle(-1, -1, cutOverflow: false);
                }
 
                if (!cellBounds.IntersectsWith(_owner.DataGridView.ClientRectangle))
                {
                    state |= AccessibleStates.Offscreen;
                }
 
                return state;
            }
        }
 
        public override string? Value
        {
            get
            {
                if (_owner is null)
                {
                    throw new InvalidOperationException(SR.DataGridViewCellAccessibleObject_OwnerNotSet);
                }
 
                object? formattedValue = _owner.FormattedValue;
                string? formattedValueAsString = formattedValue as string;
                if (formattedValue is null || (formattedValueAsString is not null && string.IsNullOrEmpty(formattedValueAsString)))
                {
                    return SR.DataGridView_AccNullValue;
                }
                else if (formattedValueAsString is not null)
                {
                    return formattedValueAsString;
                }
                else if (_owner.OwningColumn is not null)
                {
                    TypeConverter? converter = _owner.FormattedValueTypeConverter;
                    if (converter is not null && converter.CanConvertTo(typeof(string)))
                    {
                        return converter.ConvertToString(formattedValue);
                    }
                    else
                    {
                        return formattedValue.ToString();
                    }
                }
                else
                {
                    return string.Empty;
                }
            }
            set
            {
                if (_owner is null)
                {
                    throw new InvalidOperationException(SR.DataGridViewCellAccessibleObject_OwnerNotSet);
                }
 
                if (_owner is DataGridViewHeaderCell || _owner.ReadOnly || _owner.DataGridView is null || _owner.OwningRow is null)
                {
                    return;
                }
 
                if (_owner.DataGridView.IsCurrentCellInEditMode)
                {
                    // EndEdit before setting the accessible object value.
                    // This way the value being edited is validated.
                    _owner.DataGridView.EndEdit();
                }
 
                DataGridViewCellStyle dataGridViewCellStyle = _owner.InheritedStyle;
 
                // Format string "True" to boolean True.
                object? formattedValue = _owner.GetFormattedValue(
                    value,
                    _owner.OwningRow.Index,
                    ref dataGridViewCellStyle,
                    valueTypeConverter: null,
                    formattedValueTypeConverter: null,
                    DataGridViewDataErrorContexts.Formatting);
 
                // Parse the formatted value and push it into the back end.
                _owner.Value = _owner.ParseFormattedValue(
                    formattedValue,
                    dataGridViewCellStyle,
                    formattedValueTypeConverter: null,
                    valueTypeConverter: null);
            }
        }
 
        internal override bool CanGetValueInternal => false;
 
        internal override bool CanSetValueInternal => false;
 
        public override void DoDefaultAction()
        {
            if (_owner is null)
            {
                throw new InvalidOperationException(SR.DataGridViewCellAccessibleObject_OwnerNotSet);
            }
 
            DataGridViewCell dataGridViewCell = _owner;
            DataGridView? dataGridView = dataGridViewCell.DataGridView;
            if (dataGridViewCell is DataGridViewHeaderCell || dataGridView?.IsHandleCreated != true)
            {
                return;
            }
 
            if (dataGridViewCell.RowIndex == -1)
            {
                throw new InvalidOperationException(SR.DataGridView_InvalidOperationOnSharedCell);
            }
 
            Select(AccessibleSelection.TakeFocus | AccessibleSelection.TakeSelection);
            Debug.Assert(dataGridView.CurrentCell == dataGridViewCell, "the result of selecting the cell should have made this cell the current cell");
 
            if (dataGridViewCell.ReadOnly)
            {
                // don't edit if the cell is read only
                return;
            }
 
            if (dataGridViewCell.EditType is not null)
            {
                if (dataGridView.InBeginEdit || dataGridView.InEndEdit)
                {
                    // don't enter or exit editing mode if the control
                    // is in the middle of doing that already.
                    return;
                }
 
                if (dataGridView.IsCurrentCellInEditMode)
                {
                    // stop editing
                    dataGridView.EndEdit();
                }
                else if (dataGridView.EditMode != DataGridViewEditMode.EditProgrammatically)
                {
                    // start editing
                    dataGridView.BeginEdit(selectAll: true);
                }
            }
        }
 
        internal Rectangle GetAccessibleObjectBounds(AccessibleObject? parentAccObject)
        {
            if (_owner is null)
            {
                throw new InvalidOperationException(SR.DataGridViewCellAccessibleObject_OwnerNotSet);
            }
 
            if (parentAccObject is null
                || _owner.DataGridView is null
                || !_owner.DataGridView.IsHandleCreated
                || _owner.OwningColumn is null)
            {
                return Rectangle.Empty;
            }
 
            Rectangle rowRect = parentAccObject.Bounds;
            Rectangle cellRect = rowRect;
            Rectangle columnRect = _owner.DataGridView.RectangleToScreen(
                _owner.DataGridView.GetColumnDisplayRectangle(_owner.ColumnIndex, cutOverflow: false));
 
            int cellRight = columnRect.Left + columnRect.Width;
            int cellLeft = columnRect.Left;
 
            int rightToLeftRowHeadersWidth = 0;
            int leftToRightRowHeadersWidth = 0;
            if (_owner.DataGridView.RowHeadersVisible)
            {
                if (_owner.DataGridView.RightToLeft == RightToLeft.Yes)
                {
                    rightToLeftRowHeadersWidth = _owner.DataGridView.RowHeadersWidth;
                }
                else
                {
                    leftToRightRowHeadersWidth = _owner.DataGridView.RowHeadersWidth;
                }
            }
 
            if (cellLeft < rowRect.Left + leftToRightRowHeadersWidth)
            {
                cellLeft = rowRect.Left + leftToRightRowHeadersWidth;
            }
 
            cellRect.X = cellLeft;
 
            if (cellRight > rowRect.Right - rightToLeftRowHeadersWidth)
            {
                cellRight = rowRect.Right - rightToLeftRowHeadersWidth;
            }
 
            if ((cellRight - cellLeft) >= 0)
            {
                cellRect.Width = cellRight - cellLeft;
            }
            else
            {
                cellRect.Width = 0;
            }
 
            return cellRect;
        }
 
        private AccessibleObject? GetAccessibleObjectParent()
        {
            // If this is one of our types, use the shortcut provided by ParentPrivate property.
            // Otherwise, use the Parent property.
            if (_owner is DataGridViewButtonCell
                or DataGridViewCheckBoxCell
                or DataGridViewComboBoxCell
                or DataGridViewImageCell
                or DataGridViewLinkCell
                or DataGridViewTextBoxCell)
            {
                return ParentPrivate;
            }
            else
            {
                return Parent;
            }
        }
 
        public override AccessibleObject? GetChild(int index)
        {
            if (_owner is null)
            {
                throw new InvalidOperationException(SR.DataGridViewCellAccessibleObject_OwnerNotSet);
            }
 
            if (_owner.DataGridView is not null &&
                _owner.DataGridView.EditingControl is not null &&
                _owner.DataGridView.IsCurrentCellInEditMode &&
                _owner.DataGridView.CurrentCell == _owner &&
                index == 0)
            {
                return _owner.DataGridView.EditingControl.AccessibilityObject;
            }
            else
            {
                return null;
            }
        }
 
        public override int GetChildCount()
        {
            if (_owner is null)
            {
                throw new InvalidOperationException(SR.DataGridViewCellAccessibleObject_OwnerNotSet);
            }
 
            if (_owner.DataGridView is not null &&
                _owner.DataGridView.EditingControl is not null &&
                _owner.DataGridView.IsCurrentCellInEditMode &&
                _owner.DataGridView.CurrentCell == _owner)
            {
                return 1;
            }
            else
            {
                return 0;
            }
        }
 
        public override AccessibleObject? GetFocused() => null;
 
        public override AccessibleObject? GetSelected() => null;
 
        public override AccessibleObject? Navigate(AccessibleNavigation navigationDirection)
        {
            if (_owner is null)
            {
                throw new InvalidOperationException(SR.DataGridViewCellAccessibleObject_OwnerNotSet);
            }
 
            if (_owner.DataGridView?.IsHandleCreated != true || _owner.OwningColumn is null || _owner.OwningRow is null)
            {
                return null;
            }
 
            switch (navigationDirection)
            {
                case AccessibleNavigation.Right:
                    return _owner.DataGridView.RightToLeft == RightToLeft.No
                        ? NavigateForward(wrapAround: true)
                        : NavigateBackward(wrapAround: true);
 
                case AccessibleNavigation.Next:
                    return NavigateForward(wrapAround: false);
 
                case AccessibleNavigation.Left:
                    return _owner.DataGridView.RightToLeft == RightToLeft.No
                        ? NavigateBackward(wrapAround: true)
                        : NavigateForward(wrapAround: true);
 
                case AccessibleNavigation.Previous:
                    return NavigateBackward(wrapAround: false);
 
                case AccessibleNavigation.Up:
                    if (_owner.OwningRow.Index == _owner.DataGridView.Rows.GetFirstRow(DataGridViewElementStates.Visible))
                    {
                        return _owner.DataGridView.ColumnHeadersVisible
                            ? _owner.OwningColumn.HeaderCell.AccessibilityObject // Return the column header accessible object
                            : null;
                    }
                    else
                    {
                        int previousVisibleRow = _owner.DataGridView.Rows.GetPreviousRow(_owner.OwningRow.Index, DataGridViewElementStates.Visible);
                        return _owner.DataGridView.Rows[previousVisibleRow].Cells[_owner.OwningColumn.Index].AccessibilityObject;
                    }
 
                case AccessibleNavigation.Down:
                    if (_owner.OwningRow.Index == _owner.DataGridView.Rows.GetLastRow(DataGridViewElementStates.Visible))
                    {
                        return null;
                    }
                    else
                    {
                        int nextVisibleRow = _owner.DataGridView.Rows.GetNextRow(_owner.OwningRow.Index, DataGridViewElementStates.Visible);
                        return _owner.DataGridView.Rows[nextVisibleRow].Cells[_owner.OwningColumn.Index].AccessibilityObject;
                    }
 
                default:
                    return null;
            }
        }
 
        private AccessibleObject? NavigateBackward(bool wrapAround)
        {
            Debug.Assert(_owner is not null);
            Debug.Assert(_owner.DataGridView is not null);
            Debug.Assert(_owner.OwningColumn is not null);
            Debug.Assert(_owner.OwningRow is not null);
 
            if (_owner.OwningColumn == _owner.DataGridView.Columns.GetFirstColumn(DataGridViewElementStates.Visible))
            {
                if (wrapAround)
                {
                    // Return the last accessible object in the previous row
                    AccessibleObject? previousRow = _owner.OwningRow.AccessibilityObject.Navigate(AccessibleNavigation.Previous);
                    if (previousRow is null)
                    {
                        return null;
                    }
 
                    int childCount = previousRow.GetChildCount();
                    return childCount > 0 ? previousRow.GetChild(childCount - 1) : null;
                }
                else
                {
                    // return the row header cell if the row headers are visible.
                    return _owner.DataGridView.RowHeadersVisible ? _owner.OwningRow.AccessibilityObject.GetChild(0) : null;
                }
            }
            else
            {
                int previousVisibleColumnIndex = _owner.DataGridView.Columns.GetPreviousColumn(
                    _owner.OwningColumn,
                    DataGridViewElementStates.Visible,
                    DataGridViewElementStates.None)!.Index;
                return _owner.OwningRow.Cells[previousVisibleColumnIndex].AccessibilityObject;
            }
        }
 
        private AccessibleObject? NavigateForward(bool wrapAround)
        {
            Debug.Assert(_owner is not null);
            Debug.Assert(_owner.DataGridView is not null);
            Debug.Assert(_owner.OwningColumn is not null);
            Debug.Assert(_owner.OwningRow is not null);
 
            if (_owner.OwningColumn == _owner.DataGridView.Columns.GetLastColumn(
                DataGridViewElementStates.Visible,
                DataGridViewElementStates.None))
            {
                if (wrapAround)
                {
                    // Return the first cell in the next visible row.
                    AccessibleObject? nextRow = _owner.OwningRow.AccessibilityObject.Navigate(AccessibleNavigation.Next);
                    if (nextRow is not null && nextRow.GetChildCount() > 0)
                    {
                        return _owner.DataGridView.RowHeadersVisible ? nextRow.GetChild(1) : nextRow.GetChild(0);
                    }
                }
 
                return null;
            }
            else
            {
                int nextVisibleColumnIndex = _owner.DataGridView.Columns.GetNextColumn(
                    _owner.OwningColumn,
                    DataGridViewElementStates.Visible,
                    DataGridViewElementStates.None)!.Index;
                return _owner.OwningRow.Cells[nextVisibleColumnIndex].AccessibilityObject;
            }
        }
 
        public override void Select(AccessibleSelection flags)
        {
            if (_owner is null)
            {
                throw new InvalidOperationException(SR.DataGridViewCellAccessibleObject_OwnerNotSet);
            }
 
            if (_owner.DataGridView?.IsHandleCreated != true)
            {
                return;
            }
 
            if ((flags & AccessibleSelection.TakeFocus) == AccessibleSelection.TakeFocus)
            {
                _owner.DataGridView.Focus();
            }
 
            if ((flags & AccessibleSelection.TakeSelection) == AccessibleSelection.TakeSelection)
            {
                _owner.Selected = true;
                _owner.DataGridView.CurrentCell = _owner; // Do not change old selection
            }
 
            if ((flags & AccessibleSelection.AddSelection) == AccessibleSelection.AddSelection)
            {
                // it seems that in any circumstances a cell can become selected
                _owner.Selected = true;
            }
 
            if ((flags & AccessibleSelection.RemoveSelection) == AccessibleSelection.RemoveSelection &&
                (flags & (AccessibleSelection.AddSelection | AccessibleSelection.TakeSelection)) == 0)
            {
                _owner.Selected = false;
            }
        }
 
        /// <summary>
        ///  Sets the detachable child accessible object which may be added or removed to/from hierarchy nodes.
        /// </summary>
        /// <param name="child">The child accessible object.</param>
        internal override void SetDetachableChild(AccessibleObject? child)
        {
            _child = child;
        }
 
        internal override void SetFocus()
        {
            if (_owner?.DataGridView?.IsHandleCreated != true)
            {
                return;
            }
 
            base.SetFocus();
 
            RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationFocusChangedEventId);
        }
 
        internal override int[] RuntimeId => _runtimeId ??=
        [
            RuntimeIDFirstItem,
            GetHashCode()
        ];
 
        private protected override string AutomationId
        {
            get => string.Concat(RuntimeId);
        }
 
        internal override bool IsIAccessibleExSupported() => true;
 
        #region IRawElementProviderFragment Implementation
 
        internal override IRawElementProviderFragmentRoot.Interface? FragmentRoot => _owner?.DataGridView?.AccessibilityObject;
 
        internal override IRawElementProviderFragment.Interface? FragmentNavigate(NavigateDirection direction)
        {
            if (_owner is null)
            {
                throw new InvalidOperationException(SR.DataGridViewCellAccessibleObject_OwnerNotSet);
            }
 
            if (_owner.DataGridView?.IsHandleCreated != true || _owner.OwningColumn is null || _owner.OwningRow is null)
            {
                return null;
            }
 
            switch (direction)
            {
                case NavigateDirection.NavigateDirection_Parent:
                    return _owner.OwningRow.AccessibilityObject;
 
                case NavigateDirection.NavigateDirection_NextSibling:
                    return NavigateForward(wrapAround: false);
 
                case NavigateDirection.NavigateDirection_PreviousSibling:
                    return NavigateBackward(wrapAround: false);
 
                case NavigateDirection.NavigateDirection_FirstChild:
                case NavigateDirection.NavigateDirection_LastChild:
                    if (_owner.DataGridView.CurrentCell == _owner &&
                        _owner.DataGridView.IsCurrentCellInEditMode &&
                        _owner.DataGridView.EditingControl is not null)
                    {
                        return _child;
                    }
 
                    break;
 
                default:
                    return null;
            }
 
            return null;
        }
 
        #endregion
 
        #region IRawElementProviderSimple Implementation
 
        internal override unsafe VARIANT GetPropertyValue(UIA_PROPERTY_ID propertyID)
            => propertyID switch
            {
                UIA_PROPERTY_ID.UIA_ControlTypePropertyId => (VARIANT)(int)UIA_CONTROLTYPE_ID.UIA_DataItemControlTypeId,
                UIA_PROPERTY_ID.UIA_GridItemContainingGridPropertyId
                    => (VARIANT)ComHelpers.GetComPointer<IUnknown>(_owner?.DataGridView?.AccessibilityObject),
                UIA_PROPERTY_ID.UIA_HasKeyboardFocusPropertyId => (VARIANT)State.HasFlag(AccessibleStates.Focused), // Announce the cell when focusing.
                UIA_PROPERTY_ID.UIA_IsEnabledPropertyId => (VARIANT)(_owner?.DataGridView?.Enabled ?? false),
                UIA_PROPERTY_ID.UIA_IsKeyboardFocusablePropertyId => (VARIANT)State.HasFlag(AccessibleStates.Focusable),
                _ => base.GetPropertyValue(propertyID),
            };
 
        internal override bool IsPatternSupported(UIA_PATTERN_ID patternId)
        {
            if (patternId is UIA_PATTERN_ID.UIA_LegacyIAccessiblePatternId or
                UIA_PATTERN_ID.UIA_InvokePatternId or
                UIA_PATTERN_ID.UIA_ValuePatternId)
            {
                return true;
            }
 
            if ((patternId == UIA_PATTERN_ID.UIA_TableItemPatternId || patternId == UIA_PATTERN_ID.UIA_GridItemPatternId)
                // We don't want to implement patterns for header cells
                && _owner?.ColumnIndex != -1 && _owner?.RowIndex != -1)
            {
                return true;
            }
 
            return base.IsPatternSupported(patternId);
        }
 
        #endregion
 
        internal override IRawElementProviderSimple.Interface[]? GetRowHeaderItems()
        {
            if (_owner is { OwningRow.HasHeaderCell: true, DataGridView: { IsHandleCreated: true, RowHeadersVisible: true } })
            {
                return [_owner.OwningRow.HeaderCell.AccessibilityObject];
            }
 
            return null;
        }
 
        internal override IRawElementProviderSimple.Interface[]? GetColumnHeaderItems()
        {
            if (_owner is { OwningColumn.HasHeaderCell: true, DataGridView: { IsHandleCreated: true, ColumnHeadersVisible: true } })
            {
                return [_owner.OwningColumn.HeaderCell.AccessibilityObject];
            }
 
            return null;
        }
 
        internal override int Row
            => _owner?.OwningRow?.Visible is true && _owner.DataGridView is not null
                ? _owner.DataGridView.Rows.GetVisibleIndex(_owner.OwningRow)
                : -1;
 
        internal override int Column
            => _owner?.OwningColumn?.Visible is true && _owner.DataGridView is not null
                ? _owner.DataGridView.Columns.GetVisibleIndex(_owner.OwningColumn)
                : -1;
 
        internal override IRawElementProviderSimple.Interface? ContainingGrid => _owner?.DataGridView?.AccessibilityObject;
 
        internal override bool IsReadOnly => _owner?.ReadOnly ?? false;
    }
}