File: System\Windows\Forms\Controls\MonthCalendar\MonthCalendar.CalendarCellAccessibleObject.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 System.Globalization;
using Windows.Win32.System.Variant;
using Windows.Win32.UI.Accessibility;
 
namespace System.Windows.Forms;
 
public partial class MonthCalendar
{
    /// <summary>
    ///  Represents an accessible object for a calendar date cell in <see cref="MonthCalendar"/> control.
    /// </summary>
    internal class CalendarCellAccessibleObject : CalendarButtonAccessibleObject
    {
        // This const is used to get ChildId.
        // It should take into account previous cells in a row.
        // Indices start at 1.
        private const int ChildIdIncrement = 1;
 
        private readonly CalendarRowAccessibleObject _calendarRowAccessibleObject;
        private readonly CalendarBodyAccessibleObject _calendarBodyAccessibleObject;
        private readonly MonthCalendarAccessibleObject _monthCalendarAccessibleObject;
        private readonly int _calendarIndex;
        private readonly int _rowIndex;
        private readonly int _columnIndex;
        private readonly int[] _initRuntimeId;
        private SelectionRange? _dateRange;
 
        public CalendarCellAccessibleObject(CalendarRowAccessibleObject calendarRowAccessibleObject,
            CalendarBodyAccessibleObject calendarBodyAccessibleObject,
            MonthCalendarAccessibleObject monthCalendarAccessibleObject,
            int calendarIndex, int rowIndex, int columnIndex)
            : base(monthCalendarAccessibleObject)
        {
            _calendarRowAccessibleObject = calendarRowAccessibleObject;
            _calendarBodyAccessibleObject = calendarBodyAccessibleObject;
            _monthCalendarAccessibleObject = monthCalendarAccessibleObject;
            _calendarIndex = calendarIndex;
            _rowIndex = rowIndex;
            _columnIndex = columnIndex;
 
            // RuntimeId don't change if the calendar date range is not changed,
            // otherwise the calendar accessibility tree will be rebuilt.
            // So save this value one time to avoid recreating new structures and making extra calculations.
            int[] id = _calendarRowAccessibleObject.RuntimeId;
            _initRuntimeId = [id[0], id[1], id[2], id[3], id[4], GetChildId()];
        }
 
        public override Rectangle Bounds
            => _monthCalendarAccessibleObject
            .GetCalendarPartRectangle(MCGRIDINFO_PART.MCGIP_CALENDARCELL, _calendarIndex, _rowIndex, _columnIndex);
 
        internal int CalendarIndex => _calendarIndex;
 
        internal override int Column => _columnIndex;
 
        internal override IRawElementProviderSimple.Interface ContainingGrid => _calendarBodyAccessibleObject;
 
        internal virtual SelectionRange? DateRange
            => _dateRange ??= _monthCalendarAccessibleObject
            .GetCalendarPartDateRange(MCGRIDINFO_PART.MCGIP_CALENDARCELL, _calendarIndex, _rowIndex, _columnIndex);
 
        public override string? Description
        {
            get
            {
                // Only date cells in the Month view have Descriptions that based on cells date ranges
                if (!_monthCalendarAccessibleObject.IsHandleCreated
                    || _monthCalendarAccessibleObject.CalendarView != MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH
                    || DateRange is null)
                {
                    return null;
                }
 
                DateTime cellDate = DateRange.Start;
                CultureInfo culture = CultureInfo.CurrentCulture;
                int weekNumber = culture.Calendar.GetWeekOfYear(cellDate,
                    culture.DateTimeFormat.CalendarWeekRule, _monthCalendarAccessibleObject.FirstDayOfWeek);
 
                // Used string.Format here to get the correct value from resources
                // that should be consistent with the rest resources values
                return string.Format(SR.MonthCalendarWeekNumberDescription, weekNumber)
                    + $", {cellDate:dddd}";
            }
        }
 
        internal override bool CanGetDescriptionInternal => false;
 
        internal override IRawElementProviderFragment.Interface? FragmentNavigate(NavigateDirection direction)
            => direction switch
            {
                NavigateDirection.NavigateDirection_NextSibling
                    => _calendarRowAccessibleObject.CellsAccessibleObjects?.Find(this)?.Next?.Value,
                NavigateDirection.NavigateDirection_PreviousSibling
                    => _columnIndex == 0
                        ? _calendarRowAccessibleObject.WeekNumberCellAccessibleObject
                        : _calendarRowAccessibleObject.CellsAccessibleObjects?.Find(this)?.Previous?.Value,
                _ => base.FragmentNavigate(direction)
            };
 
        internal override int GetChildId() => ChildIdIncrement + _columnIndex;
 
        internal override IRawElementProviderSimple.Interface[]? GetColumnHeaderItems()
        {
            if (!_monthCalendarAccessibleObject.IsHandleCreated
                || _monthCalendarAccessibleObject.CalendarView != MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH)
            {
                // Column headers are available only in the "Month" view
                return null;
            }
 
            CalendarRowAccessibleObject? topRow = _calendarBodyAccessibleObject.RowsAccessibleObjects?.First?.Value;
 
            if (topRow is null || topRow.CellsAccessibleObjects is null)
            {
                return null;
            }
 
            foreach (CalendarCellAccessibleObject cell in topRow.CellsAccessibleObjects)
            {
                if (cell.Column == _columnIndex)
                {
                    return [cell];
                }
            }
 
            return null;
        }
 
        internal override VARIANT GetPropertyValue(UIA_PROPERTY_ID propertyID)
            => propertyID switch
            {
                UIA_PROPERTY_ID.UIA_ControlTypePropertyId => (VARIANT)(int)UIA_CONTROLTYPE_ID.UIA_DataItemControlTypeId,
                UIA_PROPERTY_ID.UIA_IsKeyboardFocusablePropertyId => (VARIANT)IsEnabled,
                _ => base.GetPropertyValue(propertyID)
            };
 
        internal override IRawElementProviderSimple.Interface[]? GetRowHeaderItems()
            => _calendarRowAccessibleObject.WeekNumberCellAccessibleObject is AccessibleObject weekNumber
                ? new IRawElementProviderSimple.Interface[1] { weekNumber }
                : null;
 
        private protected override bool HasKeyboardFocus
            => _monthCalendarAccessibleObject.Focused
                && _monthCalendarAccessibleObject.FocusedCell == this;
 
        internal override bool IsPatternSupported(UIA_PATTERN_ID patternId)
            => patternId switch
            {
                UIA_PATTERN_ID.UIA_GridItemPatternId => true,
                UIA_PATTERN_ID.UIA_TableItemPatternId => true,
                _ => base.IsPatternSupported(patternId)
            };
 
        public override string Name
        {
            get
            {
                if (DateRange is null)
                {
                    return string.Empty;
                }
 
                return _monthCalendarAccessibleObject.CalendarView switch
                {
                    MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH => $"{DateRange.Start:D}",
                    MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_YEAR => $"{DateRange.Start:Y}",
                    MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_DECADE => $"{DateRange.Start:yyy}",
                    MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_CENTURY => $"{DateRange.Start:yyy} - {DateRange.End:yyy}",
                    _ => string.Empty,
                };
            }
        }
 
        internal override bool CanGetNameInternal => false;
 
        public override AccessibleObject Parent => _calendarRowAccessibleObject;
 
        public override AccessibleRole Role => AccessibleRole.Cell;
 
        internal override int Row => _rowIndex;
 
        internal override int[] RuntimeId => _initRuntimeId;
 
        public override void Select(AccessibleSelection flags)
        {
            if (DateRange is not null)
            {
                _monthCalendarAccessibleObject.SetSelectionRange(DateRange.Start, DateRange.End);
            }
        }
 
        public override AccessibleStates State
        {
            get
            {
                AccessibleStates state = AccessibleStates.Focusable | AccessibleStates.Selectable;
 
                if (_monthCalendarAccessibleObject.Focused && _monthCalendarAccessibleObject.FocusedCell == this)
                {
                    return state | AccessibleStates.Focused | AccessibleStates.Selected;
                }
 
                // This condition works correctly in Month view only because the cell range is bigger
                // then the calendar selection range in the rest views.
                // But in the rest views a user can select only one cell. It means that a focused cell equals one selected cell,
                // so the correct state will be returned in the condition above for the rest views.
                if (DateRange is not null && _monthCalendarAccessibleObject.CalendarView == MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH
                    && DateRange.Start >= _monthCalendarAccessibleObject.SelectionRange.Start
                    && DateRange.End <= _monthCalendarAccessibleObject.SelectionRange.End)
                {
                    state |= AccessibleStates.Selected;
                }
 
                return state;
            }
        }
    }
}