|
// 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 row in <see cref="MonthCalendar"/> control.
/// </summary>
internal sealed class CalendarRowAccessibleObject : MonthCalendarChildAccessibleObject
{
// This const is used to get ChildId.
// It should take into account previous rows in a calendar body.
// Indices start at 1.
private const int ChildIdIncrement = 1;
private readonly CalendarBodyAccessibleObject _calendarBodyAccessibleObject;
private readonly MonthCalendarAccessibleObject _monthCalendarAccessibleObject;
private readonly int _calendarIndex;
private readonly int _rowIndex;
private readonly int[] _runtimeId;
private LinkedList<CalendarCellAccessibleObject>? _cellsAccessibleObjects;
private CalendarWeekNumberCellAccessibleObject? _weekNumberCellAccessibleObject;
public CalendarRowAccessibleObject(CalendarBodyAccessibleObject calendarBodyAccessibleObject,
MonthCalendarAccessibleObject monthCalendarAccessibleObject, int calendarIndex, int rowIndex)
: base(monthCalendarAccessibleObject)
{
_calendarBodyAccessibleObject = calendarBodyAccessibleObject;
_monthCalendarAccessibleObject = monthCalendarAccessibleObject;
_calendarIndex = calendarIndex;
_rowIndex = rowIndex;
// RuntimeId doesn'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 every time.
int[] id = _calendarBodyAccessibleObject.RuntimeId;
_runtimeId = [id[0], id[1], id[2], id[3], GetChildId()];
}
public override Rectangle Bounds
=> _monthCalendarAccessibleObject.GetCalendarPartRectangle(MCGRIDINFO_PART.MCGIP_CALENDARROW, _calendarIndex, _rowIndex);
// 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 _calendarIndex
// doesn't reflect that. Or we would have to get the current index of the item
// using IndexOf method every time.
internal LinkedList<CalendarCellAccessibleObject>? CellsAccessibleObjects
{
get
{
if (_cellsAccessibleObjects is null && _monthCalendarAccessibleObject.IsHandleCreated)
{
_cellsAccessibleObjects = new();
int start = 0;
// A calendar body always has 7 or 4 columns depending on its view
int end = _monthCalendarAccessibleObject.CalendarView == MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH ? 7 : 4;
for (int i = start; i < end; i++)
{
string name = _monthCalendarAccessibleObject.GetCalendarPartText(MCGRIDINFO_PART.MCGIP_CALENDARCELL, _calendarIndex, _rowIndex, i);
if (!string.IsNullOrEmpty(name))
{
CalendarCellAccessibleObject cell =
_rowIndex == -1
? new CalendarDayOfWeekCellAccessibleObject(this, _calendarBodyAccessibleObject, _monthCalendarAccessibleObject, _calendarIndex, _rowIndex, i, name)
: new CalendarCellAccessibleObject(this, _calendarBodyAccessibleObject, _monthCalendarAccessibleObject, _calendarIndex, _rowIndex, i);
_cellsAccessibleObjects.AddLast(cell);
}
}
}
return _cellsAccessibleObjects;
}
}
internal void DisconnectChildren()
{
Debug.Assert(OsVersion.IsWindows8OrGreater());
PInvoke.UiaDisconnectProvider(_weekNumberCellAccessibleObject, skipOSCheck: true);
_weekNumberCellAccessibleObject = null;
if (_cellsAccessibleObjects is null)
{
return;
}
foreach (CalendarCellAccessibleObject cell in _cellsAccessibleObjects)
{
PInvoke.UiaDisconnectProvider(cell, skipOSCheck: true);
}
_cellsAccessibleObjects.Clear();
_cellsAccessibleObjects = null;
}
public override string? Description
{
get
{
// Only day and week number cells have a description
if (_rowIndex == -1
|| _monthCalendarAccessibleObject.IsHandleCreated
|| _monthCalendarAccessibleObject.CalendarView != MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH)
{
return null;
}
// Get the first date cell date value to calculate its week number.
// It's impossible to use WeekNumberCellAccessibleObject because it may be null
// if ShowWeekNumbers is false but anyway we need to get the week number for the row.
CalendarCellAccessibleObject? cell = CellsAccessibleObjects?.First?.Value;
if (cell is null || cell.DateRange is null)
{
return null;
}
string weekNumber = GetWeekNumber(cell.DateRange.Start);
return string.Format(SR.MonthCalendarWeekNumberDescription, weekNumber);
}
}
internal override bool CanGetDescriptionInternal => false;
internal override IRawElementProviderFragment.Interface? FragmentNavigate(NavigateDirection direction)
=> direction switch
{
NavigateDirection.NavigateDirection_NextSibling
=> _calendarBodyAccessibleObject.RowsAccessibleObjects?.Find(this)?.Next?.Value,
NavigateDirection.NavigateDirection_PreviousSibling
=> _calendarBodyAccessibleObject.RowsAccessibleObjects?.Find(this)?.Previous?.Value,
NavigateDirection.NavigateDirection_FirstChild
=> _monthCalendarAccessibleObject.ShowWeekNumbers && _rowIndex != -1
? WeekNumberCellAccessibleObject
: CellsAccessibleObjects?.First?.Value,
NavigateDirection.NavigateDirection_LastChild => CellsAccessibleObjects?.Last?.Value,
_ => base.FragmentNavigate(direction)
};
internal override int GetChildId() => ChildIdIncrement + _rowIndex;
internal override VARIANT GetPropertyValue(UIA_PROPERTY_ID propertyID)
=> propertyID switch
{
UIA_PROPERTY_ID.UIA_ControlTypePropertyId => (VARIANT)(int)UIA_CONTROLTYPE_ID.UIA_PaneControlTypeId,
UIA_PROPERTY_ID.UIA_IsKeyboardFocusablePropertyId => (VARIANT)IsEnabled,
_ => base.GetPropertyValue(propertyID)
};
private string GetWeekNumber(DateTime date)
=> CultureInfo.CurrentCulture.Calendar
.GetWeekOfYear(date, CultureInfo.CurrentCulture.DateTimeFormat.CalendarWeekRule,
_monthCalendarAccessibleObject.FirstDayOfWeek).ToString();
private protected override bool HasKeyboardFocus
{
get
{
CalendarCellAccessibleObject? focusedCell = _monthCalendarAccessibleObject.FocusedCell;
return _monthCalendarAccessibleObject.Focused
&& focusedCell is not null
&& focusedCell.CalendarIndex == _calendarIndex
&& focusedCell.Row == _rowIndex;
}
}
public override string? Name => null; // Rows don't have names like in a native calendar
internal override bool CanGetNameInternal => false;
public override AccessibleObject Parent => _calendarBodyAccessibleObject;
private protected override bool IsInternal => true;
public override AccessibleRole Role => AccessibleRole.Row;
internal override int Row => _rowIndex;
internal override int[] RuntimeId => _runtimeId;
internal override void SetFocus()
{
CalendarCellAccessibleObject? focusedCell = _monthCalendarAccessibleObject.FocusedCell;
if (focusedCell is not null
&& focusedCell.CalendarIndex == _calendarIndex
&& focusedCell.Row == _rowIndex)
{
focusedCell.RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationFocusChangedEventId);
}
}
internal CalendarWeekNumberCellAccessibleObject? WeekNumberCellAccessibleObject
{
get
{
if (!_monthCalendarAccessibleObject.ShowWeekNumbers
|| _monthCalendarAccessibleObject.CalendarView != MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH
|| CellsAccessibleObjects?.First is null
|| CellsAccessibleObjects.First.Value.DateRange is null)
{
return null;
}
return _weekNumberCellAccessibleObject ??=
new(this, _calendarBodyAccessibleObject, _monthCalendarAccessibleObject, _calendarIndex,
_rowIndex, -1, GetWeekNumber(CellsAccessibleObjects.First.Value.DateRange.Start));
}
}
}
}
|