File: System\Windows\Forms\Controls\MonthCalendar\MonthCalendar.MonthCalendarAccessibleObject.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 Windows.Win32.System.Variant;
using Windows.Win32.UI.Accessibility;
 
namespace System.Windows.Forms;
 
public partial class MonthCalendar
{
    /// <summary>
    ///  Represents the accessible object of a MonthCalendar control.
    /// </summary>
    internal sealed class MonthCalendarAccessibleObject : ControlAccessibleObject
    {
        private const int MaxCalendarsCount = 12;
 
        private CalendarCellAccessibleObject? _focusedCellAccessibleObject;
        private CalendarPreviousButtonAccessibleObject? _previousButtonAccessibleObject;
        private CalendarNextButtonAccessibleObject? _nextButtonAccessibleObject;
        private LinkedList<CalendarAccessibleObject>? _calendarsAccessibleObjects;
        private CalendarTodayLinkAccessibleObject? _todayLinkAccessibleObject;
 
        public MonthCalendarAccessibleObject(MonthCalendar owner) : base(owner)
        {
            owner.DisplayRangeChanged += OnMonthCalendarStateChanged;
        }
 
        // Use a LinkedList instead a List for the following reasons:
        // 1. We don't require an access to items by indices.
        // 2. We only need the first or the last items, or iterate over all items.
        // 3. New items are only appended to the end of the collection.
        // 4. Simple API for getting an item siblings, e.g. Next or Previous values
        //    returns a real item or null.
        // 5. We have to be consistent with the rest collections of calendar parts accessible objects.
        //
        // If we use a List to store item's siblings we have to have one more variable
        // that stores a real index of the item in the collection, because _calendarIndex
        // doesn't reflect that. Or we would have to get the current index of the item
        // using IndexOf method every time.
        internal LinkedList<CalendarAccessibleObject>? CalendarsAccessibleObjects
        {
            get
            {
                if (!this.IsOwnerHandleCreated(out MonthCalendar? _))
                {
                    return null;
                }
 
                if (_calendarsAccessibleObjects is null)
                {
                    _calendarsAccessibleObjects = new();
                    string previousHeaderName = string.Empty;
 
                    for (int calendarIndex = 0; calendarIndex < MaxCalendarsCount; calendarIndex++)
                    {
                        string currentHeaderName = GetCalendarPartText(MCGRIDINFO_PART.MCGIP_CALENDARHEADER, calendarIndex);
 
                        if (currentHeaderName == string.Empty || currentHeaderName == previousHeaderName)
                        {
                            // This is a peculiarity of Win API.
                            // It returns the previous calendar name if the current one is invisible.
                            break;
                        }
 
                        CalendarAccessibleObject calendar = new(this, calendarIndex, currentHeaderName);
                        _calendarsAccessibleObjects.AddLast(calendar);
                        previousHeaderName = currentHeaderName;
                    }
                }
 
                return _calendarsAccessibleObjects;
            }
        }
 
        // This function should be called from a single place in the root of MonthCalendar object that
        // already tests for availability of this API
        internal void DisconnectChildren()
        {
            Debug.Assert(OsVersion.IsWindows8OrGreater());
 
            PInvoke.UiaDisconnectProvider(_previousButtonAccessibleObject, skipOSCheck: true);
            _previousButtonAccessibleObject = null;
 
            PInvoke.UiaDisconnectProvider(_nextButtonAccessibleObject, skipOSCheck: true);
            _nextButtonAccessibleObject = null;
 
            PInvoke.UiaDisconnectProvider(_todayLinkAccessibleObject, skipOSCheck: true);
            _todayLinkAccessibleObject = null;
 
            PInvoke.UiaDisconnectProvider(_focusedCellAccessibleObject, skipOSCheck: true);
            _focusedCellAccessibleObject = null;
 
            if (_calendarsAccessibleObjects is null)
            {
                return;
            }
 
            foreach (CalendarAccessibleObject calendarAccessibleObject in _calendarsAccessibleObjects)
            {
                calendarAccessibleObject.DisconnectChildren();
                PInvoke.UiaDisconnectProvider(calendarAccessibleObject, skipOSCheck: true);
            }
 
            _calendarsAccessibleObjects.Clear();
            _calendarsAccessibleObjects = null;
        }
 
        /// <summary>
        ///  Associates Win API day of week values with DateTime values.
        /// </summary>
        private static DayOfWeek CastDayToDayOfWeek(Day day)
            => day switch
            {
                Day.Monday => DayOfWeek.Monday,
                Day.Tuesday => DayOfWeek.Tuesday,
                Day.Wednesday => DayOfWeek.Wednesday,
                Day.Thursday => DayOfWeek.Thursday,
                Day.Friday => DayOfWeek.Friday,
                Day.Saturday => DayOfWeek.Saturday,
                Day.Sunday => DayOfWeek.Sunday,
                Day.Default => DayOfWeek.Sunday,
                _ => DayOfWeek.Sunday
            };
 
        internal MONTH_CALDENDAR_MESSAGES_VIEW CalendarView => this.TryGetOwnerAs(out MonthCalendar? owner) ? owner._mcCurView : MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH;
 
        internal override int ColumnCount
        {
            get
            {
                if (!this.IsOwnerHandleCreated(out MonthCalendar? _) || CalendarsAccessibleObjects is null)
                {
                    return -1;
                }
 
                int currentY = CalendarsAccessibleObjects.First?.Value.Bounds.Y ?? 0;
                int columnCount = 0;
 
                foreach (CalendarAccessibleObject calendar in CalendarsAccessibleObjects)
                {
                    if (calendar.Bounds.Y > currentY)
                    {
                        break;
                    }
 
                    columnCount++;
                }
 
                return columnCount;
            }
        }
 
        internal override IRawElementProviderFragment.Interface? ElementProviderFromPoint(double x, double y)
        {
            int innerX = (int)x;
            int innerY = (int)y;
 
            if (!this.IsOwnerHandleCreated(out MonthCalendar? _))
            {
                return base.ElementProviderFromPoint(x, y);
            }
 
            MCHITTESTINFO hitTestInfo = GetHitTestInfo(innerX, innerY);
 
            // See "uHit" kinds in the MS doc:
            // https://docs.microsoft.com/windows/win32/api/commctrl/ns-commctrl-mchittestinfo
            return hitTestInfo.uHit switch
            {
                MCHITTESTINFO_HIT_FLAGS.MCHT_CALENDARCONTROL => this,
                MCHITTESTINFO_HIT_FLAGS.MCHT_TITLEBTNPREV or MCHITTESTINFO_HIT_FLAGS.MCHT_PREV => PreviousButtonAccessibleObject,
                MCHITTESTINFO_HIT_FLAGS.MCHT_TITLEBTNNEXT or MCHITTESTINFO_HIT_FLAGS.MCHT_NEXT => NextButtonAccessibleObject,
                MCHITTESTINFO_HIT_FLAGS.MCHT_CALENDARDATEMIN // The given point was over the minimum date in the calendar
                    => CalendarsAccessibleObjects?.First?.Value is CalendarAccessibleObject firstCalendar
                    && firstCalendar.Bounds.Contains(innerX, innerY)
                        ? firstCalendar
                        : this,
                MCHITTESTINFO_HIT_FLAGS.MCHT_CALENDARDATEMAX // The given point was over the maximum date in the calendar
                    => CalendarsAccessibleObjects?.Last?.Value is CalendarAccessibleObject lastCalendar
                    && lastCalendar.Bounds.Contains(innerX, innerY)
                        ? lastCalendar
                        : this,
                MCHITTESTINFO_HIT_FLAGS.MCHT_CALENDAR
                    // Cast "this" to AccessibleObject because "??" can't cast these operands implicitly
                    => GetCalendarFromPoint(innerX, innerY) ?? (AccessibleObject)this,
                MCHITTESTINFO_HIT_FLAGS.MCHT_TODAYLINK => TodayLinkAccessibleObject,
                MCHITTESTINFO_HIT_FLAGS.MCHT_TITLE or MCHITTESTINFO_HIT_FLAGS.MCHT_TITLEMONTH or MCHITTESTINFO_HIT_FLAGS.MCHT_TITLEYEAR
                    // Cast "this" to AccessibleObject because "??" can't cast these operands implicitly
                    => GetCalendarFromPoint(innerX, innerY)?.CalendarHeaderAccessibleObject ?? (AccessibleObject)this,
                MCHITTESTINFO_HIT_FLAGS.MCHT_CALENDARDAY or MCHITTESTINFO_HIT_FLAGS.MCHT_CALENDARWEEKNUM or MCHITTESTINFO_HIT_FLAGS.MCHT_CALENDARDATE
                or MCHITTESTINFO_HIT_FLAGS.MCHT_CALENDARDATEPREV or MCHITTESTINFO_HIT_FLAGS.MCHT_CALENDARDATENEXT
                    // Get a calendar body's child.
                    // Cast "this" to AccessibleObject because "??" can't cast these operands implicitly
                    => GetCalendarFromPoint(innerX, innerY)?.GetChildFromPoint(hitTestInfo) ?? (AccessibleObject)this,
                MCHITTESTINFO_HIT_FLAGS.MCHT_NOWHERE => this,
                _ => base.ElementProviderFromPoint(x, y)
            };
        }
 
        internal DayOfWeek FirstDayOfWeek => this.TryGetOwnerAs(out MonthCalendar? owner) ? CastDayToDayOfWeek(owner.FirstDayOfWeek) : CastDayToDayOfWeek(Day.Default);
 
        internal bool Focused => this.TryGetOwnerAs(out MonthCalendar? owner) && owner.Focused;
 
        internal CalendarCellAccessibleObject? FocusedCell
            => _focusedCellAccessibleObject ??= this.TryGetOwnerAs(out MonthCalendar? owner) ? GetCellByDate(owner._focusedDate) : null;
 
        internal override IRawElementProviderFragment.Interface? FragmentNavigate(NavigateDirection direction)
            => direction switch
            {
                NavigateDirection.NavigateDirection_FirstChild => PreviousButtonAccessibleObject,
                NavigateDirection.NavigateDirection_LastChild => ShowToday
                    ? TodayLinkAccessibleObject
                    : CalendarsAccessibleObjects?.Last?.Value,
                _ => base.FragmentNavigate(direction),
            };
 
        private CalendarAccessibleObject? GetCalendarFromPoint(int x, int y)
        {
            if (!this.IsOwnerHandleCreated(out MonthCalendar? _) || CalendarsAccessibleObjects is null)
            {
                return null;
            }
 
            foreach (CalendarAccessibleObject calendar in CalendarsAccessibleObjects)
            {
                if (calendar.Bounds.Contains(x, y))
                {
                    return calendar;
                }
            }
 
            return null;
        }
 
        internal unsafe SelectionRange? GetCalendarPartDateRange(MCGRIDINFO_PART dwPart, int calendarIndex = 0, int rowIndex = 0, int columnIndex = 0)
        {
            if (!this.IsOwnerHandleCreated(out MonthCalendar? owner))
            {
                return null;
            }
 
            MCGRIDINFO gridInfo = new()
            {
                cbSize = (uint)sizeof(MCGRIDINFO),
                dwFlags = MCGRIDINFO_FLAGS.MCGIF_DATE,
                dwPart = dwPart,
                iCalendar = calendarIndex,
                iCol = columnIndex,
                iRow = rowIndex
            };
 
            bool success = PInvokeCore.SendMessage(owner, PInvoke.MCM_GETCALENDARGRIDINFO, 0, ref gridInfo) != 0;
 
            return success ? new((DateTime)gridInfo.stStart, (DateTime)gridInfo.stEnd) : null;
        }
 
        internal unsafe RECT GetCalendarPartRectangle(MCGRIDINFO_PART dwPart, int calendarIndex = 0, int rowIndex = 0, int columnIndex = 0)
        {
            if (!this.IsOwnerHandleCreated(out MonthCalendar? owner))
            {
                return default;
            }
 
            MCGRIDINFO gridInfo = new()
            {
                cbSize = (uint)sizeof(MCGRIDINFO),
                dwFlags = MCGRIDINFO_FLAGS.MCGIF_RECT,
                dwPart = dwPart,
                iCalendar = calendarIndex,
                iCol = columnIndex,
                iRow = rowIndex
            };
 
            bool success = PInvokeCore.SendMessage(owner, PInvoke.MCM_GETCALENDARGRIDINFO, 0, ref gridInfo) != 0;
 
            return success ? owner.RectangleToScreen(gridInfo.rc) : default;
        }
 
        internal unsafe string GetCalendarPartText(MCGRIDINFO_PART dwPart, int calendarIndex = 0, int rowIndex = 0, int columnIndex = 0)
        {
            if (!this.IsOwnerHandleCreated(out MonthCalendar? owner))
            {
                return string.Empty;
            }
 
            Span<char> name = stackalloc char[20];
 
            fixed (char* pName = name)
            {
                MCGRIDINFO gridInfo = new()
                {
                    cbSize = (uint)sizeof(MCGRIDINFO),
                    dwFlags = MCGRIDINFO_FLAGS.MCGIF_NAME,
                    dwPart = dwPart,
                    iCalendar = calendarIndex,
                    iCol = columnIndex,
                    iRow = rowIndex,
                    pszName = pName,
                    cchName = (UIntPtr)name.Length - 1
                };
 
                PInvokeCore.SendMessage(owner, PInvoke.MCM_GETCALENDARGRIDINFO, 0, ref gridInfo);
            }
 
            string text = string.Empty;
 
            foreach (char ch in name)
            {
                // Remove special invisible symbols
                if (ch is not '\0' and not (char)8206 /*empty symbol*/)
                {
                    text += ch;
                }
            }
 
            return text;
        }
 
        private CalendarCellAccessibleObject? GetCellByDate(DateTime date)
        {
            if (!this.IsOwnerHandleCreated(out MonthCalendar? _) || CalendarsAccessibleObjects is null)
            {
                return null;
            }
 
            foreach (CalendarAccessibleObject calendar in CalendarsAccessibleObjects)
            {
                if (calendar.DateRange is null)
                {
                    continue;
                }
 
                DateTime calendarStart = calendar.DateRange.Start;
                DateTime calendarEnd = calendar.DateRange.End;
 
                if (date < calendarStart || date > calendarEnd)
                {
                    continue;
                }
 
                LinkedList<CalendarRowAccessibleObject>? rows = calendar.CalendarBodyAccessibleObject.RowsAccessibleObjects;
                if (rows is null)
                {
                    return null;
                }
 
                foreach (CalendarRowAccessibleObject row in rows)
                {
                    if (row.CellsAccessibleObjects is null)
                    {
                        return null;
                    }
 
                    foreach (CalendarCellAccessibleObject cell in row.CellsAccessibleObjects)
                    {
                        // A cell date range may be null for header cells
                        SelectionRange? cellRange = cell.DateRange;
                        if (cellRange is null)
                        {
                            continue;
                        }
 
                        if (date >= cellRange.Start && date <= cellRange.End)
                        {
                            return cell;
                        }
                    }
                }
            }
 
            return null;
        }
 
        internal override IRawElementProviderSimple.Interface[]? GetColumnHeaders() => null;
 
        internal SelectionRange? GetDisplayRange(bool visible)
            => this.TryGetOwnerAs(out MonthCalendar? owner) && owner.IsHandleCreated
                ? owner.GetDisplayRange(visible)
                : null;
 
        internal override IRawElementProviderFragment.Interface? GetFocus() => _focusedCellAccessibleObject;
 
        public override AccessibleObject? GetFocused() => _focusedCellAccessibleObject;
 
        private protected override bool IsInternal => true;
 
        private unsafe MCHITTESTINFO GetHitTestInfo(int xScreen, int yScreen)
        {
            if (!this.IsOwnerHandleCreated(out MonthCalendar? owner))
            {
                return default;
            }
 
            Point point = owner.PointToClient(new Point(xScreen, yScreen));
            MCHITTESTINFO hitTestInfo = new()
            {
                cbSize = (uint)sizeof(MCHITTESTINFO),
                pt = point
            };
 
            PInvokeCore.SendMessage(owner, PInvoke.MCM_HITTEST, 0, ref hitTestInfo);
 
            return hitTestInfo;
        }
 
        internal override IRawElementProviderSimple.Interface? GetItem(int row, int column)
        {
            if (!this.IsOwnerHandleCreated(out MonthCalendar? _) || CalendarsAccessibleObjects is null)
            {
                return null;
            }
 
            foreach (CalendarAccessibleObject calendar in CalendarsAccessibleObjects)
            {
                if (calendar.Row == row && calendar.Column == column)
                {
                    return calendar;
                }
            }
 
            return null;
        }
 
        internal override VARIANT GetPropertyValue(UIA_PROPERTY_ID propertyID)
            => propertyID switch
            {
                UIA_PROPERTY_ID.UIA_ControlTypePropertyId when
                    this.TryGetOwnerAs(out MonthCalendar? owner) && owner.AccessibleRole == AccessibleRole.Default
                    => (VARIANT)(int)UIA_CONTROLTYPE_ID.UIA_CalendarControlTypeId,
                UIA_PROPERTY_ID.UIA_IsKeyboardFocusablePropertyId => (VARIANT)IsEnabled,
                _ => base.GetPropertyValue(propertyID)
            };
 
        internal override IRawElementProviderSimple.Interface[]? GetRowHeaders() => null;
 
        public override string Help
        {
            get
            {
                string? help = base.Help;
                if (help is not null)
                {
                    return help;
                }
 
                if (this.TryGetOwnerAs(out MonthCalendar? owner) && owner.GetType().BaseType is Type baseType)
                {
                    return $"{owner.GetType().Name}({baseType.Name})";
                }
 
                return string.Empty;
            }
        }
 
        internal override bool CanGetHelpInternal => false;
 
        internal bool IsEnabled => this.TryGetOwnerAs(out MonthCalendar? owner) && owner.Enabled;
 
        internal bool IsHandleCreated => this.IsOwnerHandleCreated(out MonthCalendar? _);
 
        internal override bool IsPatternSupported(UIA_PATTERN_ID patternId)
            => patternId switch
            {
                UIA_PATTERN_ID.UIA_GridPatternId => true,
                UIA_PATTERN_ID.UIA_TablePatternId => true,
                UIA_PATTERN_ID.UIA_ValuePatternId => true,
                _ => base.IsPatternSupported(patternId)
            };
 
        internal DateTime MinDate => this.TryGetOwnerAs(out MonthCalendar? owner) ? owner.MinDate : DateTime.MinValue;
        internal DateTime MaxDate => this.TryGetOwnerAs(out MonthCalendar? owner) ? owner.MaxDate : DateTime.MaxValue;
 
        internal CalendarNextButtonAccessibleObject NextButtonAccessibleObject
            => _nextButtonAccessibleObject ??= new CalendarNextButtonAccessibleObject(this);
 
        private void OnMonthCalendarStateChanged(object? sender, EventArgs e)
        {
            RebuildAccessibilityTree();
            FocusedCell?.RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationFocusChangedEventId);
        }
 
        internal CalendarPreviousButtonAccessibleObject PreviousButtonAccessibleObject
            => _previousButtonAccessibleObject ??= new CalendarPreviousButtonAccessibleObject(this);
 
        internal void RaiseAutomationEventForChild(UIA_EVENT_ID automationEventId)
        {
            if (!this.IsOwnerHandleCreated(out MonthCalendar? _))
            {
                return;
            }
 
            if (_calendarsAccessibleObjects is null)
            {
                // It means that there are no any accessibility listeners
                // that should build the accessibility tree before.
                // If we try to get the focused cell accessibility object
                // the accessibility tree will be built even a user doesn't use any accessibility tool.
                return;
            }
 
            // Update the focused cell and raise the focus event for it
            _focusedCellAccessibleObject = null;
            FocusedCell?.RaiseAutomationEvent(automationEventId);
        }
 
        private void RebuildAccessibilityTree()
        {
            if (!this.IsOwnerHandleCreated(out MonthCalendar? _) || _calendarsAccessibleObjects is null)
            {
                return;
            }
 
            if (OsVersion.IsWindows8OrGreater())
            {
                foreach (CalendarAccessibleObject calendar in _calendarsAccessibleObjects)
                {
                    calendar.DisconnectChildren();
                    PInvoke.UiaDisconnectProvider(calendar, skipOSCheck: true);
                }
 
                PInvoke.UiaDisconnectProvider(_focusedCellAccessibleObject, skipOSCheck: true);
            }
 
            _calendarsAccessibleObjects = null;
            _focusedCellAccessibleObject = null;
 
            // Recreate the calendars child collection and check if it is correct
            if (CalendarsAccessibleObjects!.Count > 0)
            {
                // Get the new focused cell accessible object and try to raise the focus event for it
                FocusedCell?.RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationFocusChangedEventId);
            }
        }
 
        public override AccessibleRole Role
            => this.GetOwnerAccessibleRole(AccessibleRole.Table);
 
        internal override int RowCount
            => ColumnCount > 0 && CalendarsAccessibleObjects is not null
                ? (int)Math.Ceiling((double)CalendarsAccessibleObjects.Count / ColumnCount)
                : 0;
 
        internal override RowOrColumnMajor RowOrColumnMajor => RowOrColumnMajor.RowOrColumnMajor_RowMajor;
 
        internal SelectionRange SelectionRange => this.TryGetOwnerAs(out MonthCalendar? owner) ? owner.SelectionRange : new SelectionRange();
 
        internal override void SetFocus()
            => FocusedCell?.RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationFocusChangedEventId);
 
        internal void SetSelectionRange(DateTime d1, DateTime d2)
        {
            if (this.IsOwnerHandleCreated(out MonthCalendar? owner))
            {
                owner.SetSelectionRange(d1, d2);
            }
        }
 
        internal bool ShowToday => this.TryGetOwnerAs(out MonthCalendar? owner) && owner.ShowToday;
 
        internal bool ShowWeekNumbers => this.TryGetOwnerAs(out MonthCalendar? owner) && owner.ShowWeekNumbers;
 
        internal DateTime TodayDate => this.TryGetOwnerAs(out MonthCalendar? owner) ? owner.TodayDate : DateTime.Today;
 
        internal CalendarTodayLinkAccessibleObject TodayLinkAccessibleObject
            => _todayLinkAccessibleObject ??= new CalendarTodayLinkAccessibleObject(this);
 
        public override string? Value
        {
            get
            {
                MonthCalendar? owner;
                SelectionRange? range;
 
                switch (CalendarView)
                {
                    case MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH:
                        if (this.TryGetOwnerAs(out owner))
                        {
                            range = owner.SelectionRange;
                            return DateTime.Equals(range.Start.Date, range.End.Date)
                                ? $"{range.Start:D}"
                                : $"{range.Start:D} - {range.End:D}";
                        }
 
                        return string.Empty;
                    case MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_YEAR:
                        if (this.TryGetOwnerAs(out owner))
                        {
                            return $"{owner.SelectionStart:y}";
                        }
 
                        return string.Empty;
                    case MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_DECADE:
                        if (this.TryGetOwnerAs(out owner))
                        {
                            return $"{owner.SelectionStart:yyyy}";
                        }
 
                        return string.Empty;
                    case MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_CENTURY:
                        range = FocusedCell?.DateRange;
                        if (range is null)
                        {
                            return string.Empty;
                        }
 
                        return $"{range.Start:yyyy} - {range.End:yyyy}";
                    default:
                        return base.Value;
                }
            }
        }
 
        internal override bool CanGetValueInternal =>
            CalendarView is not MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH
                and not MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_YEAR
                and not MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_DECADE
                and not MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_CENTURY;
 
        internal void UpdateDisplayRange()
        {
            if (!this.TryGetOwnerAs(out MonthCalendar? owner))
            {
                return;
            }
            else
            {
                owner.UpdateDisplayRange();
            }
        }
    }
}