|
// 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;
public partial class MonthCalendar
{
/// <summary>
/// Represents an accessible object for a calendar body in <see cref="MonthCalendar"/> control.
/// </summary>
internal sealed class CalendarBodyAccessibleObject : MonthCalendarChildAccessibleObject
{
// A calendar body is the second in the calendar accessibility tree.
// Indices start at 1.
private const int ChildId = 2;
private readonly CalendarAccessibleObject _calendarAccessibleObject;
private readonly MonthCalendarAccessibleObject _monthCalendarAccessibleObject;
private readonly int _calendarIndex;
private readonly string _initName;
private readonly int[] _runtimeId;
private LinkedList<CalendarRowAccessibleObject>? _rowsAccessibleObjects;
public CalendarBodyAccessibleObject(CalendarAccessibleObject calendarAccessibleObject,
MonthCalendarAccessibleObject monthCalendarAccessibleObject, int calendarIndex)
: base(monthCalendarAccessibleObject)
{
_calendarAccessibleObject = calendarAccessibleObject;
_monthCalendarAccessibleObject = monthCalendarAccessibleObject;
_calendarIndex = calendarIndex;
// Name and RuntimeId don't change if the calendar date range is not changed,
// otherwise the calendar accessibility tree will be rebuilt.
// So save these values one time to avoid sending messages to Windows every time
// or recreating new structures and making extra calculations.
_initName = _monthCalendarAccessibleObject.GetCalendarPartText(MCGRIDINFO_PART.MCGIP_CALENDARHEADER, _calendarIndex);
int[] id = _calendarAccessibleObject.RuntimeId;
_runtimeId = [id[0], id[1], id[2], GetChildId()];
}
public override Rectangle Bounds
=> _monthCalendarAccessibleObject.GetCalendarPartRectangle(MCGRIDINFO_PART.MCGIP_CALENDARBODY, _calendarIndex);
internal void DisconnectChildren()
{
Debug.Assert(OsVersion.IsWindows8OrGreater());
if (_rowsAccessibleObjects is null)
{
return;
}
foreach (CalendarRowAccessibleObject row in _rowsAccessibleObjects)
{
row.DisconnectChildren();
PInvoke.UiaDisconnectProvider(row, skipOSCheck: true);
}
_rowsAccessibleObjects.Clear();
_rowsAccessibleObjects = null;
}
/// <remark>
/// A calendar always have 7 or 4 columns depending on its view.
/// </remark>
internal override int ColumnCount => _monthCalendarAccessibleObject.CalendarView == MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH ? 7 : 4;
internal override IRawElementProviderFragment.Interface? FragmentNavigate(NavigateDirection direction)
=> direction switch
{
NavigateDirection.NavigateDirection_NextSibling => null,
NavigateDirection.NavigateDirection_PreviousSibling => _calendarAccessibleObject.CalendarHeaderAccessibleObject,
NavigateDirection.NavigateDirection_FirstChild => RowsAccessibleObjects?.First?.Value,
NavigateDirection.NavigateDirection_LastChild => RowsAccessibleObjects?.Last?.Value,
_ => base.FragmentNavigate(direction),
};
internal override int GetChildId() => ChildId;
internal override IRawElementProviderSimple.Interface[]? GetColumnHeaders()
{
// A calendar has column headers (days of week) only in the Month view
if (_monthCalendarAccessibleObject.CalendarView != MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH)
{
return null;
}
return RowsAccessibleObjects?.First?.Value.CellsAccessibleObjects?.ToArray();
}
internal override IRawElementProviderSimple.Interface? GetItem(int rowIndex, int columnIndex)
{
if (!_monthCalendarAccessibleObject.IsHandleCreated || RowsAccessibleObjects is null)
{
return null;
}
CalendarRowAccessibleObject? rowAccessibleObject = null;
foreach (CalendarRowAccessibleObject row in RowsAccessibleObjects)
{
if (row.Row == rowIndex)
{
rowAccessibleObject = row;
break;
}
}
if (rowAccessibleObject is null)
{
return null;
}
if (rowIndex >= 0 && columnIndex == -1)
{
return rowAccessibleObject.WeekNumberCellAccessibleObject;
}
if (rowAccessibleObject.CellsAccessibleObjects is null)
{
return null;
}
foreach (CalendarCellAccessibleObject cell in rowAccessibleObject.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_TableControlTypeId,
UIA_PROPERTY_ID.UIA_IsKeyboardFocusablePropertyId => (VARIANT)IsEnabled,
_ => base.GetPropertyValue(propertyID)
};
/// <remark>
/// A calendar has row headers (week numbers) if <see cref="ShowWeekNumbers"/> is true
/// and the calendar in the Month view only.
/// </remark>
internal override IRawElementProviderSimple.Interface[]? GetRowHeaders()
{
if (!_monthCalendarAccessibleObject.IsHandleCreated
|| !_monthCalendarAccessibleObject.ShowWeekNumbers
|| _monthCalendarAccessibleObject.CalendarView != MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH
|| RowsAccessibleObjects is null)
{
return null;
}
List<CalendarCellAccessibleObject> headers = [];
foreach (CalendarRowAccessibleObject row in RowsAccessibleObjects)
{
if (row.Row == -1)
{
continue;
}
if (row.WeekNumberCellAccessibleObject is null)
{
Debug.Fail($"{nameof(row.WeekNumberCellAccessibleObject)} must not be null if ShowWeekNumbers is true in the Month view!");
return null;
}
headers.Add(row.WeekNumberCellAccessibleObject);
}
return headers.ToArray();
}
private protected override bool HasKeyboardFocus
=> _monthCalendarAccessibleObject.Focused
&& _monthCalendarAccessibleObject.FocusedCell?.CalendarIndex == _calendarIndex;
internal override bool IsPatternSupported(UIA_PATTERN_ID patternId)
=> patternId switch
{
UIA_PATTERN_ID.UIA_GridPatternId => true,
UIA_PATTERN_ID.UIA_TablePatternId => true,
_ => base.IsPatternSupported(patternId)
};
public override string Name => _initName;
internal override bool CanGetNameInternal => false;
public override AccessibleObject Parent => _calendarAccessibleObject;
private protected override bool IsInternal => true;
public override AccessibleRole Role => AccessibleRole.Table;
internal override int RowCount => RowsAccessibleObjects?.Count ?? -1;
internal override RowOrColumnMajor RowOrColumnMajor => RowOrColumnMajor.RowOrColumnMajor_RowMajor;
// 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.
//
// 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 _rowIndex
// doesn't reflect that. Or we would have to get the current index of the item
// using IndexOf method every time.
internal LinkedList<CalendarRowAccessibleObject>? RowsAccessibleObjects
{
get
{
if (_rowsAccessibleObjects is null && _monthCalendarAccessibleObject.IsHandleCreated)
{
_rowsAccessibleObjects = new();
// Day of week cells have "-1" row index
int start = _monthCalendarAccessibleObject.CalendarView == MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH ? -1 : 0;
// A calendar body always has 6 or 3 columns depending on its view
int end = _monthCalendarAccessibleObject.CalendarView == MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH ? 6 : 3;
for (int i = start; i < end; i++)
{
// Don't add a row if it doesn't have cells
CalendarRowAccessibleObject row = new(this, _monthCalendarAccessibleObject, _calendarIndex, i);
if (row.CellsAccessibleObjects?.Count > 0)
{
_rowsAccessibleObjects.AddLast(row);
}
}
}
return _rowsAccessibleObjects;
}
}
internal override int[] RuntimeId => _runtimeId;
internal override void SetFocus()
{
CalendarCellAccessibleObject? focusedCell = _monthCalendarAccessibleObject.FocusedCell;
if (focusedCell?.CalendarIndex == _calendarIndex)
{
focusedCell.RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationFocusChangedEventId);
}
}
public override AccessibleStates State => AccessibleStates.Default;
}
}
|