// 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 ? [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; } } } }