|
// 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();
}
}
}
}
|