diff --git a/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.IMultipleViewProvider.cs b/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.IMultipleViewProvider.cs new file mode 100644 index 00000000000..205b65c9d24 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.IMultipleViewProvider.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class UiaCore + { + [ComImport] + [Guid("6278cab1-b556-4a1a-b4e0-418acc523201")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IMultipleViewProvider + { + string? GetViewName(int viewId); + + void SetCurrentView(int viewId); + + int CurrentView { get; } + + int[]? GetSupportedViews(); + } + } +} diff --git a/src/System.Windows.Forms/src/Resources/SR.resx b/src/System.Windows.Forms/src/Resources/SR.resx index b9c99e75383..a2bdcb28a86 100644 --- a/src/System.Windows.Forms/src/Resources/SR.resx +++ b/src/System.Windows.Forms/src/Resources/SR.resx @@ -6722,4 +6722,7 @@ Stack trace where the illegal operation occurred was: TextFormatFlags.ModifyString is not allowed. + + Double Click + diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.cs.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.cs.xlf index f7af6dc7f11..e7623cbe0d6 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.cs.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.cs.xlf @@ -192,6 +192,11 @@ Sbalit + + Double Click + Double Click + + Expand Rozbalit diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.de.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.de.xlf index ac2dfd022b2..02436bc4774 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.de.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.de.xlf @@ -192,6 +192,11 @@ Zuklappen + + Double Click + Double Click + + Expand Aufklappen diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.es.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.es.xlf index 1db8c39e1c2..1083ca764e2 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.es.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.es.xlf @@ -192,6 +192,11 @@ Contraer + + Double Click + Double Click + + Expand Expandir diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.fr.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.fr.xlf index 06d53011a76..531bb201f1b 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.fr.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.fr.xlf @@ -192,6 +192,11 @@ Réduire + + Double Click + Double Click + + Expand Développer diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.it.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.it.xlf index 9044eeb3b6e..707b63014c5 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.it.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.it.xlf @@ -192,6 +192,11 @@ Comprimi + + Double Click + Double Click + + Expand Espandi diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.ja.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.ja.xlf index a8bcad455df..2295425ea39 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.ja.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.ja.xlf @@ -192,6 +192,11 @@ 折りたたむ + + Double Click + Double Click + + Expand 展開 diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.ko.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.ko.xlf index 61e46fbd902..b61dc85f2a7 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.ko.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.ko.xlf @@ -192,6 +192,11 @@ 축소 + + Double Click + Double Click + + Expand 확장 diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.pl.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.pl.xlf index c9169bd11ff..fdd598310d5 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.pl.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.pl.xlf @@ -192,6 +192,11 @@ Zwiń + + Double Click + Double Click + + Expand Rozwiń diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.pt-BR.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.pt-BR.xlf index bd6716e7969..e90b3cb3951 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.pt-BR.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.pt-BR.xlf @@ -192,6 +192,11 @@ Recolher + + Double Click + Double Click + + Expand Expandir diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.ru.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.ru.xlf index 0db028371ca..3f6925868c4 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.ru.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.ru.xlf @@ -192,6 +192,11 @@ Свернуть + + Double Click + Double Click + + Expand Развертывание diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.tr.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.tr.xlf index 0c61115532f..e8d3137871e 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.tr.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.tr.xlf @@ -192,6 +192,11 @@ Daralt + + Double Click + Double Click + + Expand Genişlet diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hans.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hans.xlf index dcc9c56ade1..c2d48c6e0cd 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hans.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hans.xlf @@ -192,6 +192,11 @@ 折叠 + + Double Click + Double Click + + Expand 展开 diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hant.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hant.xlf index db2e4132abc..c3b0921f0b9 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hant.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hant.xlf @@ -192,6 +192,11 @@ 摺疊 + + Double Click + Double Click + + Expand 展開 diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/AccessibleObject.cs b/src/System.Windows.Forms/src/System/Windows/Forms/AccessibleObject.cs index 6c11dd851f5..6ddf229ee3f 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/AccessibleObject.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/AccessibleObject.cs @@ -42,7 +42,8 @@ public partial class AccessibleObject : UiaCore.ISelectionProvider, UiaCore.ISelectionItemProvider, UiaCore.IRawElementProviderHwndOverride, - UiaCore.IScrollItemProvider + UiaCore.IScrollItemProvider, + UiaCore.IMultipleViewProvider { /// /// Specifies the interface used by this . @@ -482,6 +483,16 @@ internal virtual void SetValue(string? newValue) internal virtual UiaCore.IRawElementProviderSimple? GetOverrideProviderForHwnd(IntPtr hwnd) => null; + internal virtual int GetMultiViewProviderCurrentView() => 0; + + internal virtual int[]? GetMultiViewProviderSupportedViews() => Array.Empty(); + + internal virtual string GetMultiViewProviderViewName(int viewId) => string.Empty; + + internal virtual void SetMultiViewProviderCurrentView(int viewId) + { + } + internal virtual void SetValue(double newValue) { } @@ -1782,6 +1793,14 @@ MemberInfo[] IReflect.GetMembers(BindingFlags bindingAttr) UiaCore.IRawElementProviderSimple? UiaCore.IRawElementProviderHwndOverride.GetOverrideProviderForHwnd(IntPtr hwnd) => GetOverrideProviderForHwnd(hwnd); + int UiaCore.IMultipleViewProvider.CurrentView => GetMultiViewProviderCurrentView(); + + int[]? UiaCore.IMultipleViewProvider.GetSupportedViews() => GetMultiViewProviderSupportedViews(); + + string? UiaCore.IMultipleViewProvider.GetViewName(int viewId) => GetMultiViewProviderViewName(viewId); + + void UiaCore.IMultipleViewProvider.SetCurrentView(int viewId) => SetMultiViewProviderCurrentView(viewId); + BOOL UiaCore.IRangeValueProvider.IsReadOnly => IsReadOnly ? BOOL.TRUE : BOOL.FALSE; double UiaCore.IRangeValueProvider.LargeChange => LargeChange; diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/ColumnHeader.ListViewColumnHeaderAccessibleObject.cs b/src/System.Windows.Forms/src/System/Windows/Forms/ColumnHeader.ListViewColumnHeaderAccessibleObject.cs new file mode 100644 index 00000000000..c1f8b472c55 --- /dev/null +++ b/src/System.Windows.Forms/src/System/Windows/Forms/ColumnHeader.ListViewColumnHeaderAccessibleObject.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using static Interop.UiaCore; + +namespace System.Windows.Forms +{ + public partial class ColumnHeader + { + internal class ListViewColumnHeaderAccessibleObject : AccessibleObject + { + private readonly ColumnHeader _owningColumnHeader; + + public ListViewColumnHeaderAccessibleObject(ColumnHeader columnHeader) + { + _owningColumnHeader = columnHeader ?? throw new ArgumentNullException(nameof(columnHeader)); + } + + internal override object? GetPropertyValue(UIA propertyID) + => propertyID switch + { + UIA.ControlTypePropertyId => UIA.HeaderItemControlTypeId, + UIA.NamePropertyId => _owningColumnHeader.Text, + _ => base.GetPropertyValue(propertyID) + }; + } + } +} diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/ColumnHeader.cs b/src/System.Windows.Forms/src/System/Windows/Forms/ColumnHeader.cs index 4e58843fd68..b4a34cfa1d3 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/ColumnHeader.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/ColumnHeader.cs @@ -19,7 +19,7 @@ namespace System.Windows.Forms [DesignTimeVisible(false)] [DefaultProperty(nameof(Text))] [TypeConverter(typeof(ColumnHeaderConverter))] - public class ColumnHeader : Component, ICloneable + public partial class ColumnHeader : Component, ICloneable { // disable csharp compiler warning #0414: field assigned unused value #pragma warning disable 0414 @@ -32,6 +32,7 @@ public class ColumnHeader : Component, ICloneable // Use TextAlign property instead of this member variable, always private HorizontalAlignment _textAlign = HorizontalAlignment.Left; private bool _textAlignInitialized; + private AccessibleObject _accessibilityObject; private readonly ColumnHeaderImageListIndexer _imageIndexer; // We need to send some messages to ListView when it gets initialized. @@ -76,6 +77,19 @@ public ColumnHeader(string imageKey) : this() ImageKey = imageKey; } + internal AccessibleObject AccessibilityObject + { + get + { + if (_accessibilityObject is null) + { + _accessibilityObject = new ListViewColumnHeaderAccessibleObject(this); + } + + return _accessibilityObject; + } + } + internal int ActualImageIndex_Internal { get diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/InternalAccessibleObject.cs b/src/System.Windows.Forms/src/System/Windows/Forms/InternalAccessibleObject.cs index d40c6e8a7fb..dedf92aba1b 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/InternalAccessibleObject.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/InternalAccessibleObject.cs @@ -39,7 +39,8 @@ internal sealed class InternalAccessibleObject : ISelectionProvider, ISelectionItemProvider, IScrollItemProvider, - IRawElementProviderHwndOverride + IRawElementProviderHwndOverride, + IMultipleViewProvider { private IAccessible publicIAccessible; // AccessibleObject as IAccessible private readonly Oleaut32.IEnumVariant publicIEnumVariant; // AccessibleObject as Oleaut32.IEnumVariant @@ -67,6 +68,7 @@ internal sealed class InternalAccessibleObject : private readonly ISelectionItemProvider publicISelectionItemProvider; // AccessibleObject as ISelectionItemProvider private readonly IScrollItemProvider publicIScrollItemProvider; // AccessibleObject as IScrollItemProvider private readonly IRawElementProviderHwndOverride publicIRawElementProviderHwndOverride; // AccessibleObject as IRawElementProviderHwndOverride + private readonly IMultipleViewProvider publicIMultiViewProvider; // AccessibleObject as IMultipleViewProvider /// /// Create a new wrapper. @@ -97,6 +99,7 @@ internal InternalAccessibleObject(AccessibleObject accessibleImplemention) publicISelectionItemProvider = (ISelectionItemProvider)accessibleImplemention; publicIScrollItemProvider = (IScrollItemProvider)accessibleImplemention; publicIRawElementProviderHwndOverride = (IRawElementProviderHwndOverride)accessibleImplemention; + publicIMultiViewProvider = (IMultipleViewProvider)accessibleImplemention; // Note: Deliberately not holding onto AccessibleObject to enforce all access through the interfaces } @@ -289,70 +292,30 @@ ProviderOptions IRawElementProviderSimple.ProviderOptions object? IRawElementProviderSimple.GetPatternProvider(UIA patternId) { object? obj = publicIRawElementProviderSimple.GetPatternProvider(patternId); - if (obj != null) - { - // we always want to return the internal accessible object - if (patternId == UIA.ExpandCollapsePatternId) - { - return (IExpandCollapseProvider)this; - } - else if (patternId == UIA.ValuePatternId) - { - return (IValueProvider)this; - } - else if (patternId == UIA.RangeValuePatternId) - { - return (IRangeValueProvider)this; - } - else if (patternId == UIA.TogglePatternId) - { - return (IToggleProvider)this; - } - else if (patternId == UIA.TablePatternId) - { - return (ITableProvider)this; - } - else if (patternId == UIA.TableItemPatternId) - { - return (ITableItemProvider)this; - } - else if (patternId == UIA.GridPatternId) - { - return (IGridProvider)this; - } - else if (patternId == UIA.GridItemPatternId) - { - return (IGridItemProvider)this; - } - else if (patternId == UIA.InvokePatternId) - { - return (IInvokeProvider)this; - } - else if (patternId == UIA.LegacyIAccessiblePatternId) - { - return (ILegacyIAccessibleProvider)this; - } - else if (patternId == UIA.SelectionPatternId) - { - return (ISelectionProvider)this; - } - else if (patternId == UIA.SelectionItemPatternId) - { - return (ISelectionItemProvider)this; - } - else if (patternId == UIA.ScrollItemPatternId) - { - return (IScrollItemProvider)this; - } - else - { - return null; - } - } - else + if (obj is null) { return null; } + + // we always want to return the internal accessible object + return patternId switch + { + UIA.ExpandCollapsePatternId => (IExpandCollapseProvider)this, + UIA.ValuePatternId => (IValueProvider)this, + UIA.RangeValuePatternId => (IRangeValueProvider)this, + UIA.TogglePatternId => (IToggleProvider)this, + UIA.TablePatternId => (ITableProvider)this, + UIA.TableItemPatternId => (ITableItemProvider)this, + UIA.GridPatternId => (IGridProvider)this, + UIA.GridItemPatternId => (IGridItemProvider)this, + UIA.InvokePatternId => (IInvokeProvider)this, + UIA.LegacyIAccessiblePatternId => (ILegacyIAccessibleProvider)this, + UIA.SelectionPatternId => (ISelectionProvider)this, + UIA.SelectionItemPatternId => (ISelectionItemProvider)this, + UIA.ScrollItemPatternId => (IScrollItemProvider)this, + UIA.MultipleViewPatternId => (IMultipleViewProvider)this, + _ => null + }; } object? IRawElementProviderSimple.GetPropertyValue(UIA propertyID) @@ -537,5 +500,17 @@ ExpandCollapseState IExpandCollapseProvider.ExpandCollapseState /// Return the provider for the specified component, or null if the component is not being overridden. IRawElementProviderSimple? IRawElementProviderHwndOverride.GetOverrideProviderForHwnd(IntPtr hwnd) => publicIRawElementProviderHwndOverride.GetOverrideProviderForHwnd(hwnd); + + int IMultipleViewProvider.CurrentView + => publicIMultiViewProvider.CurrentView; + + int[]? IMultipleViewProvider.GetSupportedViews() + => publicIMultiViewProvider.GetSupportedViews(); + + string? IMultipleViewProvider.GetViewName(int viewId) + => publicIMultiViewProvider.GetViewName(viewId); + + void IMultipleViewProvider.SetCurrentView(int viewId) + => publicIMultiViewProvider.SetCurrentView(viewId); } } diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/ListView.ListViewAccessibleObject.cs b/src/System.Windows.Forms/src/System/Windows/Forms/ListView.ListViewAccessibleObject.cs new file mode 100644 index 00000000000..88ac6937c83 --- /dev/null +++ b/src/System.Windows.Forms/src/System/Windows/Forms/ListView.ListViewAccessibleObject.cs @@ -0,0 +1,340 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Drawing; +using static Interop; + +namespace System.Windows.Forms +{ + public partial class ListView + { + internal class ListViewAccessibleObject : ControlAccessibleObject + { + private readonly ListView _owningListView; + private static readonly int[] s_enumViewValues = (int[])Enum.GetValues(typeof(View)); + + internal ListViewAccessibleObject(ListView owningListView) : base(owningListView) + { + _owningListView = owningListView; + } + + internal override bool CanSelectMultiple + => _owningListView.IsHandleCreated; + + internal override int ColumnCount + => _owningListView.Columns.Count; + + private bool OwnerHasDefaultGroup + { + get + { + if (!_owningListView.IsHandleCreated || !_owningListView.ShowGroups) + { + return false; + } + + foreach (ListViewItem? item in _owningListView.Items) + { + // If there are groups in the ListView, then the items which do not belong + // to any of the group and have null as the item.Group value, so these items + // are put into the default group and thereby the ListView itself starts + // containing Default group. + if (item != null && item.Group is null) + { + return true; + } + } + + return false; + } + } + + private bool OwnerHasGroups + => _owningListView.IsHandleCreated && _owningListView.Groups.Count > 0; + + internal override int RowCount + => _owningListView.Items.Count; + + internal override UiaCore.RowOrColumnMajor RowOrColumnMajor + => UiaCore.RowOrColumnMajor.RowMajor; + + internal override int[]? RuntimeId + { + get + { + if (!_owningListView.IsHandleCreated) + { + return base.RuntimeId; + } + + var runtimeId = new int[2]; + runtimeId[0] = RuntimeIDFirstItem; + runtimeId[1] = PARAM.ToInt(_owningListView.Handle); + return runtimeId; + } + } + + internal override UiaCore.IRawElementProviderFragment ElementProviderFromPoint(double x, double y) + { + AccessibleObject? element = HitTest((int)x, (int)y); + + return element ?? base.ElementProviderFromPoint(x, y); + } + + internal override UiaCore.IRawElementProviderFragment? FragmentNavigate(UiaCore.NavigateDirection direction) + { + if (!_owningListView.IsHandleCreated) + { + return null; + } + + int childCount = GetChildCount(); + if (childCount == 0) + { + return null; + } + + return direction switch + { + UiaCore.NavigateDirection.FirstChild => GetChild(0), + UiaCore.NavigateDirection.LastChild => GetChild(childCount - 1), + _ => base.FragmentNavigate(direction) + }; + } + + public override AccessibleObject? GetChild(int index) + { + if (!_owningListView.IsHandleCreated || index < 0 || index >= GetChildCount()) + { + return null; + } + + if (!OwnerHasGroups) + { + return _owningListView.Items[index].AccessibilityObject; + } + + if (!OwnerHasDefaultGroup) + { + return _owningListView.Groups[index].AccessibilityObject; + } + + // Default group has the last index out of the Groups.Count + // upper bound: so the DefaultGroup.Index == Groups.Count. + // But IMPORTANT: in the accessible tree the position of + // default group is the first before other groups. + return index == 0 + ? _owningListView.DefaultGroup.AccessibilityObject + : _owningListView.Groups[index - 1].AccessibilityObject; + } + + public override int GetChildCount() + { + if (!_owningListView.IsHandleCreated) + { + return 0; + } + + if (_owningListView.Groups.Count > 0) + { + return OwnerHasDefaultGroup ? _owningListView.Groups.Count + 1 : _owningListView.Groups.Count; + } + + return _owningListView.Items.Count; + } + + internal int GetChildIndex(AccessibleObject child) + { + if (child is null) + { + return -1; + } + + int childCount = GetChildCount(); + for (int i = 0; i < childCount; i++) + { + AccessibleObject? currentChild = GetChild(i); + if (child == currentChild) + { + return i; + } + } + + return -1; + } + + private string GetItemStatus() + => _owningListView.Sorting switch + { + SortOrder.Ascending => SR.SortedAscendingAccessibleStatus, + SortOrder.Descending => SR.SortedDescendingAccessibleStatus, + _ => SR.NotSortedAccessibleStatus + }; + + internal override UiaCore.IRawElementProviderSimple[]? GetColumnHeaders() + { + UiaCore.IRawElementProviderSimple[] columnHeaders = new UiaCore.IRawElementProviderSimple[_owningListView.Columns.Count]; + for (int i = 0; i < columnHeaders.Length; i++) + { + ColumnHeader columnHeader = _owningListView.Columns[i]; + columnHeaders[i] = new ColumnHeader.ListViewColumnHeaderAccessibleObject(columnHeader); + } + + return columnHeaders; + } + + internal override UiaCore.IRawElementProviderFragment? GetFocus() + { + if (!_owningListView.IsHandleCreated) + { + return null; + } + + return _owningListView.FocusedItem?.AccessibilityObject ?? _owningListView.FocusedGroup?.AccessibilityObject; + } + + internal override int GetMultiViewProviderCurrentView() + => (int)_owningListView.View; + + internal override int[] GetMultiViewProviderSupportedViews() + => new int[] { (int)View.Details }; + + internal override string GetMultiViewProviderViewName(int viewId) + { + foreach (int view in s_enumViewValues) + { + if (view == viewId) + { + return view.ToString(); + } + } + + return string.Empty; + } + + internal AccessibleObject? GetNextChild(AccessibleObject currentChild) + { + int currentChildIndex = GetChildIndex(currentChild); + if (currentChildIndex == -1) + { + return null; + } + + int childCount = GetChildCount(); + if (currentChildIndex > childCount - 2) // is not the second to the last element. + { + return null; + } + + return GetChild(currentChildIndex + 1); + } + + internal AccessibleObject? GetPreviousChild(AccessibleObject currentChild) + { + int currentChildIndex = GetChildIndex(currentChild); + if (currentChildIndex <= 0) + { + return null; + } + + return GetChild(currentChildIndex - 1); + } + + internal override object? GetPropertyValue(UiaCore.UIA propertyID) + => propertyID switch + { + UiaCore.UIA.NamePropertyId => Name, + UiaCore.UIA.AutomationIdPropertyId => _owningListView.Name, + UiaCore.UIA.IsKeyboardFocusablePropertyId => (State & AccessibleStates.Focusable) == AccessibleStates.Focusable, + UiaCore.UIA.HasKeyboardFocusPropertyId => false, + UiaCore.UIA.RuntimeIdPropertyId => RuntimeId, + UiaCore.UIA.ControlTypePropertyId => UiaCore.UIA.ListControlTypeId, + UiaCore.UIA.IsMultipleViewPatternAvailablePropertyId => IsPatternSupported(UiaCore.UIA.MultipleViewPatternId), + UiaCore.UIA.ItemStatusPropertyId => GetItemStatus(), + _ => base.GetPropertyValue(propertyID) + }; + + internal override UiaCore.IRawElementProviderSimple[]? GetRowHeaders() + => null; + + internal override UiaCore.IRawElementProviderSimple[] GetSelection() + { + if (!_owningListView.IsHandleCreated) + { + return Array.Empty(); + } + + UiaCore.IRawElementProviderSimple[] selectedItemProviders = new UiaCore.IRawElementProviderSimple[_owningListView.SelectedItems.Count]; + for (int i = 0; i < selectedItemProviders.Length; i++) + { + selectedItemProviders[i] = _owningListView.SelectedItems[i].AccessibilityObject; + } + + return selectedItemProviders; + } + + public override AccessibleObject? HitTest(int x, int y) + { + if (!_owningListView.IsHandleCreated) + { + return null; + } + + Point point = _owningListView.PointToClient(new Point(x, y)); + ListViewHitTestInfo hitTestInfo = _owningListView.HitTest(point.X, point.Y); + if (hitTestInfo.Item is null && OwnerHasGroups) + { + for (int i = 0; i < GetChildCount(); i++) + { + AccessibleObject? accessibilityObject = GetChild(i); + if (accessibilityObject != null && + accessibilityObject.Bounds.Contains(new Point(x, y))) + { + return accessibilityObject; + } + } + + return null; + } + + if (hitTestInfo.Item != null) + { + if (hitTestInfo.SubItem != null) + { + return hitTestInfo.SubItem.AccessibilityObject; + } + + return hitTestInfo.Item.AccessibilityObject; + } + + return null; + } + + internal override bool IsPatternSupported(UiaCore.UIA patternId) + { + if (patternId == UiaCore.UIA.SelectionPatternId || + patternId == UiaCore.UIA.MultipleViewPatternId || + (patternId == UiaCore.UIA.GridPatternId && _owningListView.View == View.Details) || + (patternId == UiaCore.UIA.TablePatternId && _owningListView.View == View.Details)) + { + return true; + } + + return base.IsPatternSupported(patternId); + } + + internal override void SetMultiViewProviderCurrentView(int viewId) + { + foreach (var view in s_enumViewValues) + { + if (view == viewId) + { + _owningListView.View = (View)view; + } + } + } + } + } +} diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/ListView.cs b/src/System.Windows.Forms/src/System/Windows/Forms/ListView.cs index 68f1447053e..a38f2751205 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/ListView.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/ListView.cs @@ -15,6 +15,7 @@ using System.Runtime.InteropServices; using System.Windows.Forms.Layout; using System.Windows.Forms.VisualStyles; +using static System.Windows.Forms.ListViewGroup; using static Interop; using static Interop.ComCtl32; @@ -29,7 +30,7 @@ namespace System.Windows.Forms [DefaultProperty(nameof(Items))] [DefaultEvent(nameof(SelectedIndexChanged))] [SRDescription(nameof(SR.DescriptionListView))] - public class ListView : Control + public partial class ListView : Control { //members private const int MASK_HITTESTFLAG = 0x00F7; @@ -127,6 +128,7 @@ public class ListView : Control private int virtualListSize; private ListViewGroup defaultGroup; + private ListViewGroup focusedGroup; // Invariant: the table always contains all Items in the ListView, and maps IDs -> Items. // listItemsArray is null if the handle is created; otherwise, it contains all Items. @@ -777,7 +779,9 @@ internal ListViewGroup DefaultGroup if (defaultGroup is null) { defaultGroup = new ListViewGroup(string.Format(SR.ListViewGroupDefaultGroup, "1")); + defaultGroup.ListView = this; } + return defaultGroup; } } @@ -815,6 +819,24 @@ internal bool ExpectingMouseUp } } + /// + /// Retreives the group which currently has the user focus. This is the + /// group that's drawn with the dotted focus rectangle around it. + /// Returns null if no group is currently focused. + /// + internal ListViewGroup FocusedGroup + { + get => IsHandleCreated ? focusedGroup : null; + set + { + if (IsHandleCreated && value != null) + { + value.Focused = true; + focusedGroup = value; + } + } + } + /// /// Retreives the item which currently has the user focus. This is the /// item that's drawn with the dotted focus rectangle around it. @@ -1671,6 +1693,8 @@ public ImageList StateImageList } } + internal override bool SupportsUiaProviders => true; + [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] [Bindable(false)] @@ -3492,6 +3516,21 @@ internal int GetColumnIndex(ColumnHeader ch) return -1; } + internal int GetGroupIndex(ListViewGroup owningGroup) + { + if (Groups.Count == 0) + { + return -1; + } + // Default group it's last group in accessibility and not specified in Groups collection, therefore default group index = Groups.Count + if (DefaultGroup == owningGroup) + { + return Groups.Count; + } + + return Groups.IndexOf(owningGroup); + } + /// /// Returns the current ListViewItem corresponding to the specific /// x,y co-ordinate. @@ -4822,6 +4861,12 @@ protected virtual void OnSearchForVirtualItem(SearchForVirtualItemEventArgs e) protected virtual void OnSelectedIndexChanged(EventArgs e) { ((EventHandler)Events[EVENT_SELECTEDINDEXCHANGED])?.Invoke(this, e); + + if (SelectedItems.Count > 0 && SelectedItems[0].Focused) + { + AccessibleObject accessibilityObject = SelectedItems[0].AccessibilityObject; + accessibilityObject?.RaiseAutomationEvent(UiaCore.UIA.AutomationFocusChangedEventId); + } } protected override void OnSystemColorsChanged(EventArgs e) @@ -5857,6 +5902,10 @@ private void WmMouseDown(ref Message m, MouseButtons button, int clicks) DefWndProc(ref m); } } + + Point screenPoint = PointToScreen(new Point(x, y)); + AccessibleObject accessibilityObject = AccessibilityObject.HitTest(screenPoint.X, screenPoint.Y); + accessibilityObject?.RaiseAutomationEvent(UiaCore.UIA.AutomationFocusChangedEventId); } private unsafe bool WmNotify(ref Message m) @@ -6511,6 +6560,26 @@ private unsafe void WmReflectNotify(ref Message m) break; case (int)LVN.KEYDOWN: + if (Groups.Count > 0) + { + NMLVKEYDOWN* lvkd = (NMLVKEYDOWN*)m.LParam; + if (lvkd->wVKey == (short)Keys.Down + && SelectedItems.Count > 0 + && SelectedItems[0].AccessibilityObject.FragmentNavigate(UiaCore.NavigateDirection.NextSibling) is null) + { + ListViewGroupAccessibleObject groupAccObj = (ListViewGroupAccessibleObject)SelectedItems[0].AccessibilityObject.FragmentNavigate(UiaCore.NavigateDirection.Parent); + ListViewGroupAccessibleObject nextGroupAccObj = (ListViewGroupAccessibleObject)groupAccObj.FragmentNavigate(UiaCore.NavigateDirection.NextSibling); + nextGroupAccObj?.SetFocus(); + } + + if (lvkd->wVKey == (short)Keys.Up + && SelectedItems.Count > 0 + && SelectedItems[0].AccessibilityObject.FragmentNavigate(UiaCore.NavigateDirection.PreviousSibling) is null) + { + ListViewGroupAccessibleObject groupAccObj = (ListViewGroupAccessibleObject)SelectedItems[0].AccessibilityObject.FragmentNavigate(UiaCore.NavigateDirection.Parent); + groupAccObj?.SetFocus(); + } + } if (CheckBoxes) { NMLVKEYDOWN* lvkd = (NMLVKEYDOWN*)m.LParam; @@ -9795,43 +9864,5 @@ protected override AccessibleObject CreateAccessibilityInstance() { return new ListViewAccessibleObject(this); } - - internal class ListViewAccessibleObject : ControlAccessibleObject - { - private readonly ListView owner; - - internal ListViewAccessibleObject(ListView owner) : base(owner) - { - this.owner = owner; - } - - internal override bool IsIAccessibleExSupported() - { - if (owner != null) - { - return true; - } - - return base.IsIAccessibleExSupported(); - } - - internal override object GetPropertyValue(UiaCore.UIA propertyID) - { - if (propertyID == UiaCore.UIA.ItemStatusPropertyId) - { - switch (owner.Sorting) - { - case SortOrder.None: - return SR.NotSortedAccessibleStatus; - case SortOrder.Ascending: - return SR.SortedAscendingAccessibleStatus; - case SortOrder.Descending: - return SR.SortedDescendingAccessibleStatus; - } - } - - return base.GetPropertyValue(propertyID); - } - } } } diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/ListViewGroup.ListViewGroupAccessibleObject.cs b/src/System.Windows.Forms/src/System/Windows/Forms/ListViewGroup.ListViewGroupAccessibleObject.cs new file mode 100644 index 00000000000..74185dfde68 --- /dev/null +++ b/src/System.Windows.Forms/src/System/Windows/Forms/ListViewGroup.ListViewGroupAccessibleObject.cs @@ -0,0 +1,282 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Drawing; +using static Interop; +using static Interop.ComCtl32; + +namespace System.Windows.Forms +{ + public partial class ListViewGroup + { + internal class ListViewGroupAccessibleObject : AccessibleObject + { + private readonly ListView _owningListView; + private readonly ListViewGroup _owningGroup; + private readonly bool _owningGroupIsDefault; + + public ListViewGroupAccessibleObject(ListViewGroup owningGroup, bool owningGroupIsDefault) + { + _owningGroup = owningGroup ?? throw new ArgumentNullException(nameof(owningGroup)); + _owningListView = owningGroup.ListView ?? throw new InvalidOperationException(nameof(owningGroup.ListView)); + _owningGroupIsDefault = owningGroupIsDefault; + } + + private string AutomationId + => string.Format("{0}-{1}", typeof(ListViewGroup).Name, CurrentIndex); + + public override Rectangle Bounds + { + get + { + if (!_owningListView.IsHandleCreated) + { + return Rectangle.Empty; + } + + RECT groupRect = new RECT(); + User32.SendMessageW(_owningListView, (User32.WM)ComCtl32.LVM.GETGROUPRECT, (IntPtr)CurrentIndex, ref groupRect); + + return new Rectangle( + _owningListView.AccessibilityObject.Bounds.X + groupRect.left, + _owningListView.AccessibilityObject.Bounds.Y + groupRect.top, + groupRect.right - groupRect.left, + groupRect.bottom - groupRect.top); + } + } + + private int CurrentIndex + => _owningGroupIsDefault + // Default group has the last index out of the Groups.Count + // upper bound: so the DefaultGroup.Index == Groups.Count. + ? _owningListView.Groups.Count + : _owningListView.Groups.IndexOf(_owningGroup); + + public override string DefaultAction + => SR.AccessibleActionDoubleClick; + + internal override UiaCore.ExpandCollapseState ExpandCollapseState + => _owningGroup.CollapsedState == ListViewGroupCollapsedState.Expanded + ? UiaCore.ExpandCollapseState.Expanded + : UiaCore.ExpandCollapseState.Collapsed; + + public override string Name + => _owningGroup.Header; + + public override AccessibleRole Role + => AccessibleRole.Grouping; + + internal override int[]? RuntimeId + { + get + { + var owningListViewRuntimeId = _owningListView.AccessibilityObject.RuntimeId; + if (owningListViewRuntimeId is null) + { + return base.RuntimeId; + } + + var runtimeId = new int[4]; + runtimeId[0] = owningListViewRuntimeId[0]; + runtimeId[1] = owningListViewRuntimeId[1]; + runtimeId[2] = 4; // Win32-control specific RuntimeID constant, is used in similar Win32 controls and is used in WinForms controls for consistency. + runtimeId[3] = CurrentIndex; + return runtimeId; + } + } + + public override AccessibleStates State + { + get + { + AccessibleStates state = AccessibleStates.Focusable; + + if (Focused) + { + state |= AccessibleStates.Focused; + } + + return state; + } + } + + internal override void Collapse() + => _owningGroup.CollapsedState = ListViewGroupCollapsedState.Collapsed; + + public override void DoDefaultAction() => SetFocus(); + + internal override void Expand() + => _owningGroup.CollapsedState = ListViewGroupCollapsedState.Expanded; + + private bool Focused + => GetNativeFocus() || _owningGroup.Focused; + + private bool GetNativeFocus() + { + if (!_owningListView.IsHandleCreated) + { + return false; + } + + return LVGS.FOCUSED == unchecked((LVGS)(long)User32.SendMessageW(_owningListView, (User32.WM)LVM.GETGROUPSTATE, (IntPtr)CurrentIndex, (IntPtr)LVGS.FOCUSED)); + } + + internal override object? GetPropertyValue(UiaCore.UIA propertyID) + => propertyID switch + { + UiaCore.UIA.RuntimeIdPropertyId => RuntimeId, + UiaCore.UIA.AutomationIdPropertyId => AutomationId, + UiaCore.UIA.BoundingRectanglePropertyId => Bounds, + UiaCore.UIA.LegacyIAccessibleRolePropertyId => Role, + UiaCore.UIA.LegacyIAccessibleNamePropertyId => Name, + UiaCore.UIA.FrameworkIdPropertyId => NativeMethods.WinFormFrameworkId, + UiaCore.UIA.ControlTypePropertyId => UiaCore.UIA.GroupControlTypeId, + UiaCore.UIA.NamePropertyId => Name, + UiaCore.UIA.HasKeyboardFocusPropertyId => _owningListView.Focused && Focused, + UiaCore.UIA.IsKeyboardFocusablePropertyId => (State & AccessibleStates.Focusable) == AccessibleStates.Focusable, + UiaCore.UIA.IsEnabledPropertyId => _owningListView.Enabled, + UiaCore.UIA.IsOffscreenPropertyId => (State & AccessibleStates.Offscreen) == AccessibleStates.Offscreen, + UiaCore.UIA.NativeWindowHandlePropertyId => _owningListView.IsHandleCreated ? _owningListView.Handle : IntPtr.Zero, + UiaCore.UIA.IsLegacyIAccessiblePatternAvailablePropertyId => IsPatternSupported(UiaCore.UIA.LegacyIAccessiblePatternId), + _ => base.GetPropertyValue(propertyID) + }; + + internal override UiaCore.IRawElementProviderFragment? FragmentNavigate(UiaCore.NavigateDirection direction) + { + switch (direction) + { + case UiaCore.NavigateDirection.Parent: + return _owningListView.AccessibilityObject; + case UiaCore.NavigateDirection.NextSibling: + return (_owningListView.AccessibilityObject as ListView.ListViewAccessibleObject)?.GetNextChild(this); + case UiaCore.NavigateDirection.PreviousSibling: + return (_owningListView.AccessibilityObject as ListView.ListViewAccessibleObject)?.GetPreviousChild(this); + case UiaCore.NavigateDirection.FirstChild: + int childCount = GetChildCount(); + if (childCount > 0) + { + return GetChild(0); + } + + return null; + case UiaCore.NavigateDirection.LastChild: + childCount = GetChildCount(); + if (childCount > 0) + { + return GetChild(childCount - 1); + } + + return null; + default: + return null; + } + } + + public override AccessibleObject? GetChild(int index) + { + if (!_owningGroupIsDefault) + { + if (index < 0 || index >= _owningGroup.Items.Count) + { + return null; + } + + return _owningGroup.Items[index].AccessibilityObject; + } + + foreach (ListViewItem? item in _owningListView.Items) + { + if (item != null && item.Group is null && index-- == 0) + { + return item.AccessibilityObject; + } + } + + return null; + } + + private int GetChildIndex(AccessibleObject child) + { + int childCount = GetChildCount(); + for (int i = 0; i < childCount; i++) + { + var currentChild = GetChild(i); + if (child == currentChild) + { + return i; + } + } + + return -1; + } + + internal AccessibleObject? GetNextChild(AccessibleObject currentChild) + { + int currentChildIndex = GetChildIndex(currentChild); + if (currentChildIndex == -1) + { + return null; + } + + int childCount = GetChildCount(); + if (currentChildIndex > childCount - 2) // Is more than pre-last element. + { + return null; + } + + return GetChild(currentChildIndex + 1); + } + + internal AccessibleObject? GetPreviousChild(AccessibleObject currentChild) + { + int currentChildIndex = GetChildIndex(currentChild); + if (currentChildIndex <= 0) + { + return null; + } + + return GetChild(currentChildIndex - 1); + } + + public override int GetChildCount() + { + if (_owningGroupIsDefault) + { + int count = 0; + foreach (ListViewItem? item in _owningListView.Items) + { + if (item != null && item.Group is null) + { + count++; + } + } + + return count; + } + else + { + return _owningGroup.Items.Count; + } + } + + internal override bool IsPatternSupported(UiaCore.UIA patternId) + { + if (patternId == UiaCore.UIA.LegacyIAccessiblePatternId || + patternId == UiaCore.UIA.ExpandCollapsePatternId) + { + return true; + } + + return base.IsPatternSupported(patternId); + } + + internal override unsafe void SetFocus() + { + _owningListView.FocusedGroup = _owningGroup; + + RaiseAutomationEvent(UiaCore.UIA.AutomationFocusChangedEventId); + } + } + } +} diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/ListViewGroup.cs b/src/System.Windows.Forms/src/System/Windows/Forms/ListViewGroup.cs index e5b6441025e..1ea562dc7db 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/ListViewGroup.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/ListViewGroup.cs @@ -17,7 +17,7 @@ namespace System.Windows.Forms [DesignTimeVisible(false)] [DefaultProperty(nameof(Header))] [Serializable] // This type is participating in resx serialization scenarios. - public sealed class ListViewGroup : ISerializable + public sealed partial class ListViewGroup : ISerializable { private string? _header; private HorizontalAlignment _headerAlignment = HorizontalAlignment.Left; @@ -34,6 +34,7 @@ public sealed class ListViewGroup : ISerializable private static int s_nextHeader = 1; private ListViewGroupImageIndexer? _imageIndexer; + private AccessibleObject? _accessibilityObject; /// /// Creates a ListViewGroup. @@ -76,6 +77,19 @@ public ListViewGroup(string? header, HorizontalAlignment headerAlignment) : this _headerAlignment = headerAlignment; } + internal AccessibleObject? AccessibilityObject + { + get + { + if (_accessibilityObject is null) + { + _accessibilityObject = new ListViewGroupAccessibleObject(this, ListView?.Groups.Contains(this) == false); + } + + return _accessibilityObject; + } + } + /// /// The text displayed in the group header. /// @@ -172,6 +186,8 @@ public HorizontalAlignment FooterAlignment } } + internal bool Focused { get; set; } + /// /// Controls which the group will appear as. /// diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/ListViewItem.ListViewItemAccessibleObject.cs b/src/System.Windows.Forms/src/System/Windows/Forms/ListViewItem.ListViewItemAccessibleObject.cs new file mode 100644 index 00000000000..966e31ec611 --- /dev/null +++ b/src/System.Windows.Forms/src/System/Windows/Forms/ListViewItem.ListViewItemAccessibleObject.cs @@ -0,0 +1,324 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Drawing; +using System.Runtime.InteropServices; +using Accessibility; +using static System.Windows.Forms.ListView; +using static System.Windows.Forms.ListViewGroup; +using static Interop; + +namespace System.Windows.Forms +{ + public partial class ListViewItem + { + internal class ListViewItemAccessibleObject : AccessibleObject + { + private readonly ListView _owningListView; + private readonly ListViewItem _owningItem; + private readonly ListViewGroup? _owningGroup; + private readonly IAccessible? _systemIAccessible; + + public ListViewItemAccessibleObject(ListViewItem owningItem, ListViewGroup? owningGroup) + { + _owningItem = owningItem ?? throw new ArgumentNullException(nameof(owningItem)); + _owningListView = owningItem.ListView ?? throw new InvalidOperationException(nameof(owningItem.ListView)); + _owningGroup = owningGroup; + _systemIAccessible = _owningListView.AccessibilityObject.GetSystemIAccessibleInternal(); + } + + private string AutomationId + => string.Format("{0}-{1}", typeof(ListViewItem).Name, CurrentIndex); + + public override Rectangle Bounds + => new Rectangle( + _owningListView.AccessibilityObject.Bounds.X + _owningItem.Bounds.X, + _owningListView.AccessibilityObject.Bounds.Y + _owningItem.Bounds.Y, + _owningItem.Bounds.Width, + _owningItem.Bounds.Height); + + private int CurrentIndex + => _owningListView.Items.IndexOf(_owningItem); + + internal override UiaCore.IRawElementProviderFragmentRoot FragmentRoot + => _owningListView.AccessibilityObject; + + internal override bool IsItemSelected + => (State & AccessibleStates.Selected) != 0; + + public override string? Name + { + get => _owningItem.Text; + set => base.Name = value; + } + + private bool OwningListItemFocused + { + get + { + bool owningListViewFocused = _owningListView.Focused; + bool owningListItemFocused = _owningListView.FocusedItem == _owningItem; + return owningListViewFocused && owningListItemFocused; + } + } + + /// + /// Gets the accessible role. + /// + public override AccessibleRole Role + => AccessibleRole.ListItem; + + /// + /// Gets the accessible state. + /// + public override AccessibleStates State + { + get + { + AccessibleStates state = AccessibleStates.Selectable | AccessibleStates.Focusable | AccessibleStates.MultiSelectable; + + if (_owningListView.SelectedItems.Contains(_owningItem)) + { + return state |= AccessibleStates.Selected | AccessibleStates.Focused; + } + + if (_systemIAccessible != null) + { + return state |= (AccessibleStates)(_systemIAccessible.get_accState(GetChildId())); + } + + return state; + } + } + + internal override void AddToSelection() + => SelectItem(); + + public override string DefaultAction + => SR.AccessibleActionDoubleClick; + + public override void DoDefaultAction() + => SetFocus(); + + internal override UiaCore.IRawElementProviderFragment? FragmentNavigate(UiaCore.NavigateDirection direction) + { + ListViewGroupAccessibleObject? owningGroupAccessibleObject = (ListViewGroupAccessibleObject?)_owningGroup?.AccessibilityObject; + switch (direction) + { + case UiaCore.NavigateDirection.Parent: + return owningGroupAccessibleObject ?? _owningListView.AccessibilityObject; + case UiaCore.NavigateDirection.NextSibling: + if (owningGroupAccessibleObject is null) + { + ListViewAccessibleObject listViewAccessibilityObject = (ListViewAccessibleObject)_owningListView.AccessibilityObject; + return listViewAccessibilityObject.GetNextChild(this); + } + + return owningGroupAccessibleObject.GetNextChild(this); + case UiaCore.NavigateDirection.PreviousSibling: + if (owningGroupAccessibleObject is null) + { + ListViewAccessibleObject listViewAccessibilityObject = (ListViewAccessibleObject)_owningListView.AccessibilityObject; + return listViewAccessibilityObject.GetPreviousChild(this); + } + + return owningGroupAccessibleObject.GetPreviousChild(this); + case UiaCore.NavigateDirection.FirstChild: + if (_owningItem.SubItems.Count > 0) + { + return GetChild(0); + } + break; + case UiaCore.NavigateDirection.LastChild: + int subItemsCount = _owningItem.SubItems.Count; + if (subItemsCount > 0) + { + return GetChild(subItemsCount - 1); + } + break; + } + + return base.FragmentNavigate(direction); + } + + public override AccessibleObject? GetChild(int index) + { + if (index < 0 || index >= _owningItem.SubItems.Count || _owningGroup != null) + { + return null; + } + + return _owningItem.SubItems[index].AccessibilityObject; + } + + internal override int[]? RuntimeId + { + get + { + int[] runtimeId; + var owningListViewRuntimeId = _owningListView.AccessibilityObject.RuntimeId; + if (owningListViewRuntimeId is null) + { + return base.RuntimeId; + } + + if (_owningGroup != null) + { + runtimeId = new int[5]; + runtimeId[0] = owningListViewRuntimeId[0]; + runtimeId[1] = owningListViewRuntimeId[1]; + runtimeId[2] = 4; // Win32-control specific RuntimeID constant, is used in similar Win32 controls and is used in WinForms controls for consistency. + runtimeId[3] = _owningListView.GetGroupIndex(_owningGroup); + runtimeId[4] = CurrentIndex; + + return runtimeId; + } + + runtimeId = new int[4]; + runtimeId[0] = owningListViewRuntimeId[0]; + runtimeId[1] = owningListViewRuntimeId[1]; + runtimeId[2] = 4; // Win32-control specific RuntimeID constant. + runtimeId[3] = CurrentIndex; + + return runtimeId; + } + } + + internal override object? GetPropertyValue(UiaCore.UIA propertyID) + => propertyID switch + { + UiaCore.UIA.RuntimeIdPropertyId => RuntimeId, + UiaCore.UIA.AutomationIdPropertyId => AutomationId, + UiaCore.UIA.BoundingRectanglePropertyId => Bounds, + UiaCore.UIA.FrameworkIdPropertyId => NativeMethods.WinFormFrameworkId, + UiaCore.UIA.ControlTypePropertyId => UiaCore.UIA.ListItemControlTypeId, + UiaCore.UIA.NamePropertyId => Name, + UiaCore.UIA.HasKeyboardFocusPropertyId => OwningListItemFocused, + UiaCore.UIA.IsKeyboardFocusablePropertyId => (State & AccessibleStates.Focusable) == AccessibleStates.Focusable, + UiaCore.UIA.IsEnabledPropertyId => _owningListView.Enabled, + UiaCore.UIA.IsOffscreenPropertyId => (State & AccessibleStates.Offscreen) == AccessibleStates.Offscreen, + UiaCore.UIA.NativeWindowHandlePropertyId => _owningListView.IsHandleCreated ? _owningListView.Handle : IntPtr.Zero, + UiaCore.UIA.IsSelectionItemPatternAvailablePropertyId => IsPatternSupported(UiaCore.UIA.SelectionItemPatternId), + UiaCore.UIA.IsScrollItemPatternAvailablePropertyId => IsPatternSupported(UiaCore.UIA.ScrollItemPatternId), + UiaCore.UIA.IsInvokePatternAvailablePropertyId => IsPatternSupported(UiaCore.UIA.InvokePatternId), + _ => base.GetPropertyValue(propertyID) + }; + + /// + /// Indicates whether specified pattern is supported. + /// + /// The pattern ID. + /// True if specified + internal override bool IsPatternSupported(UiaCore.UIA patternId) + { + if (patternId == UiaCore.UIA.ScrollItemPatternId || + patternId == UiaCore.UIA.LegacyIAccessiblePatternId || + patternId == UiaCore.UIA.SelectionItemPatternId || + patternId == UiaCore.UIA.InvokePatternId) + { + return true; + } + + return base.IsPatternSupported(patternId); + } + + internal override void RemoveFromSelection() + { + // Do nothing, C++ implementation returns UIA_E_INVALIDOPERATION 0x80131509 + } + + internal override UiaCore.IRawElementProviderSimple ItemSelectionContainer + => _owningListView.AccessibilityObject; + + internal override void ScrollIntoView() + { + if (!_owningListView.IsHandleCreated) + { + return; + } + + int currentIndex = CurrentIndex; + + if (_owningListView.SelectedItems is null) // no items selected + { + User32.SendMessageW(_owningListView, (User32.WM)User32.LB.SETCARETINDEX, (IntPtr)currentIndex); + return; + } + + int firstVisibleIndex = (int)(long)User32.SendMessageW(_owningListView, (User32.WM)User32.LB.GETTOPINDEX); + if (currentIndex < firstVisibleIndex) + { + User32.SendMessageW(_owningListView, (User32.WM)User32.LB.SETTOPINDEX, (IntPtr)currentIndex); + return; + } + + int itemsHeightSum = 0; + int listBoxHeight = _owningListView.ClientRectangle.Height; + int itemsCount = _owningListView.Items.Count; + + for (int i = firstVisibleIndex; i < itemsCount; i++) + { + int itemHeight = PARAM.ToInt(User32.SendMessageW(_owningListView, (User32.WM)User32.LB.GETITEMHEIGHT, (IntPtr)i)); + + itemsHeightSum += itemHeight; + + if (itemsHeightSum <= listBoxHeight) + { + continue; + } + + int lastVisibleIndex = i - 1; // less 1 because last "i" index is invisible + int visibleItemsCount = lastVisibleIndex - firstVisibleIndex + 1; // add 1 because array indexes begin with 0 + + if (currentIndex > lastVisibleIndex) + { + User32.SendMessageW(_owningListView, (User32.WM)User32.LB.SETTOPINDEX, (IntPtr)(currentIndex - visibleItemsCount + 1)); + } + + break; + } + } + + internal unsafe override void SelectItem() + { + if (_owningListView.IsHandleCreated) + { + _owningListView.SelectedIndices.Add(CurrentIndex); + User32.InvalidateRect(new HandleRef(this, _owningListView.Handle), null, BOOL.FALSE); + } + + RaiseAutomationEvent(UiaCore.UIA.AutomationFocusChangedEventId); + RaiseAutomationEvent(UiaCore.UIA.SelectionItem_ElementSelectedEventId); + } + + internal override void SetFocus() + { + RaiseAutomationEvent(UiaCore.UIA.AutomationFocusChangedEventId); + SelectItem(); + } + + public override void Select(AccessibleSelection flags) + { + if (!_owningListView.IsHandleCreated) + { + return; + } + + try + { + _systemIAccessible?.accSelect((int)flags, GetChildId()); + } + catch (ArgumentException) + { + // In Everett, the ListBox accessible children did not have any selection capability. + // In Whidbey, they delegate the selection capability to OLEACC. + // However, OLEACC does not deal w/ several Selection flags: ExtendSelection, AddSelection, RemoveSelection. + // OLEACC instead throws an ArgumentException. + // Since Whidbey API's should not throw an exception in places where Everett API's did not, we catch + // the ArgumentException and fail silently. + } + } + } + } +} diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/ListViewItem.ListViewSubItem.ListViewSubItemAccessibleObject.cs b/src/System.Windows.Forms/src/System/Windows/Forms/ListViewItem.ListViewSubItem.ListViewSubItemAccessibleObject.cs new file mode 100644 index 00000000000..4dcc9ec0c39 --- /dev/null +++ b/src/System.Windows.Forms/src/System/Windows/Forms/ListViewItem.ListViewSubItem.ListViewSubItemAccessibleObject.cs @@ -0,0 +1,165 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Drawing; +using static Interop; + +namespace System.Windows.Forms +{ + public partial class ListViewItem + { + public partial class ListViewSubItem + { + internal class ListViewSubItemAccessibleObject : AccessibleObject + { + private readonly ListView _owningListView; + private readonly ListViewItem _owningItem; + private readonly ListViewSubItem _owningSubItem; + + public ListViewSubItemAccessibleObject(ListViewSubItem owningSubItem, ListViewItem owningItem) + { + _owningSubItem = owningSubItem ?? throw new ArgumentNullException(nameof(owningSubItem)); + _owningItem = owningItem ?? throw new ArgumentNullException(nameof(owningItem)); + _owningListView = owningItem.ListView ?? throw new InvalidOperationException(nameof(owningItem.ListView)); + } + + internal override UiaCore.IRawElementProviderFragmentRoot FragmentRoot + => _owningListView.AccessibilityObject; + + public override Rectangle Bounds + { + get + { + // Previously bounds was provided using MSAA, + // but using UIA we found out that SendMessageW work incorrectly. + // When we need to get bounds for first sub item it will return width of all item. + int width = _owningSubItem.Bounds.Width; + + if (Column == 0 && _owningItem.SubItems.Count > 1) + { + width = _owningItem.SubItems[Column + 1].Bounds.X - _owningSubItem.Bounds.X; + } + + return new Rectangle( + _owningListView.AccessibilityObject.Bounds.X + _owningSubItem.Bounds.X, + _owningListView.AccessibilityObject.Bounds.Y + _owningSubItem.Bounds.Y, + width, _owningSubItem.Bounds.Height); + } + } + + internal override UiaCore.IRawElementProviderFragment? FragmentNavigate(UiaCore.NavigateDirection direction) + { + switch (direction) + { + case UiaCore.NavigateDirection.Parent: + return _owningItem.AccessibilityObject; + case UiaCore.NavigateDirection.NextSibling: + int nextSubItemIndex = GetCurrentSubItemIndex() + 1; + if (_owningItem.SubItems.Count > nextSubItemIndex) + { + return _owningItem.SubItems[nextSubItemIndex].AccessibilityObject; + } + break; + case UiaCore.NavigateDirection.PreviousSibling: + int previousSubItemIndex = GetCurrentSubItemIndex() - 1; + if (previousSubItemIndex >= 0) + { + return _owningItem.SubItems[previousSubItemIndex].AccessibilityObject; + } + break; + } + + return base.FragmentNavigate(direction); + } + + /// + /// Gets or sets the accessible name. + /// + public override string? Name + { + get => base.Name ?? _owningSubItem.Text; + set => base.Name = value; + } + + public override AccessibleObject Parent + => _owningItem.AccessibilityObject; + + internal override int[]? RuntimeId + { + get + { + var owningItemRuntimeId = Parent.RuntimeId; + if (owningItemRuntimeId is null) + { + return base.RuntimeId; + } + + var runtimeId = new int[5]; + runtimeId[0] = owningItemRuntimeId[0]; + runtimeId[1] = owningItemRuntimeId[1]; + runtimeId[2] = owningItemRuntimeId[2]; + runtimeId[3] = owningItemRuntimeId[3]; + runtimeId[4] = GetCurrentSubItemIndex(); + return runtimeId; + } + } + + internal override object? GetPropertyValue(UiaCore.UIA propertyID) + => propertyID switch + { + UiaCore.UIA.ControlTypePropertyId => UiaCore.UIA.TextControlTypeId, + UiaCore.UIA.NamePropertyId => Name, + UiaCore.UIA.FrameworkIdPropertyId => NativeMethods.WinFormFrameworkId, + UiaCore.UIA.ProcessIdPropertyId => Process.GetCurrentProcess().Id, + UiaCore.UIA.AutomationIdPropertyId => AutomationId, + UiaCore.UIA.RuntimeIdPropertyId => RuntimeId, + UiaCore.UIA.HasKeyboardFocusPropertyId => _owningListView.Focused && _owningListView.FocusedItem == _owningItem, + UiaCore.UIA.IsKeyboardFocusablePropertyId => (State & AccessibleStates.Focusable) == AccessibleStates.Focusable, + UiaCore.UIA.IsEnabledPropertyId => _owningListView.Enabled, + UiaCore.UIA.IsOffscreenPropertyId => (State & AccessibleStates.Offscreen) == AccessibleStates.Offscreen, + UiaCore.UIA.BoundingRectanglePropertyId => Bounds, + UiaCore.UIA.IsGridItemPatternAvailablePropertyId => IsPatternSupported(UiaCore.UIA.GridItemPatternId), + UiaCore.UIA.IsTableItemPatternAvailablePropertyId => IsPatternSupported(UiaCore.UIA.TableItemPatternId), + _ => base.GetPropertyValue(propertyID) + }; + + /// + /// Gets the accessible state. + /// + public override AccessibleStates State + => AccessibleStates.Focusable; + + internal override UiaCore.IRawElementProviderSimple ContainingGrid + => _owningListView.AccessibilityObject; + + internal override int Row + => _owningItem.Index; + + internal override int Column + => _owningItem.SubItems.IndexOf(_owningSubItem); + + internal override UiaCore.IRawElementProviderSimple[]? GetColumnHeaderItems() + => new UiaCore.IRawElementProviderSimple[] { _owningListView.Columns[Column].AccessibilityObject }; + + internal override bool IsPatternSupported(UiaCore.UIA patternId) + { + if (patternId == UiaCore.UIA.GridItemPatternId || + patternId == UiaCore.UIA.TableItemPatternId) + { + return true; + } + + return base.IsPatternSupported(patternId); + } + + private string AutomationId + => string.Format("{0}-{1}", typeof(ListViewItem.ListViewSubItem).Name, GetCurrentSubItemIndex()); + + private int GetCurrentSubItemIndex() + => _owningItem.SubItems.IndexOf(_owningSubItem); + } + } + } +} diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/ListViewItem.ListViewSubItem.cs b/src/System.Windows.Forms/src/System/Windows/Forms/ListViewItem.ListViewSubItem.cs new file mode 100644 index 00000000000..fd9ea270f35 --- /dev/null +++ b/src/System.Windows.Forms/src/System/Windows/Forms/ListViewItem.ListViewSubItem.cs @@ -0,0 +1,291 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#nullable disable + +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Runtime.Serialization; + +namespace System.Windows.Forms +{ + public partial class ListViewItem + { + [TypeConverter(typeof(ListViewSubItemConverter))] + [ToolboxItem(false)] + [DesignTimeVisible(false)] + [DefaultProperty(nameof(Text))] + [Serializable] // This type is participating in resx serialization scenarios. + public partial class ListViewSubItem + { + [NonSerialized] + internal ListViewItem owner; +#pragma warning disable IDE1006 + private string text; // Do NOT rename (binary serialization). + + [OptionalField(VersionAdded = 2)] + private string name = null; // Do NOT rename (binary serialization). + + private SubItemStyle style; // Do NOT rename (binary serialization). + + [OptionalField(VersionAdded = 2)] + private object userData; // Do NOT rename (binary serialization). +#pragma warning restore IDE1006 + + [NonSerialized] + private AccessibleObject _accessibilityObject; + + public ListViewSubItem() + { + } + + public ListViewSubItem(ListViewItem owner, string text) + { + this.owner = owner; + this.text = text; + } + + public ListViewSubItem(ListViewItem owner, string text, Color foreColor, Color backColor, Font font) + { + this.owner = owner; + this.text = text; + style = new SubItemStyle + { + foreColor = foreColor, + backColor = backColor, + font = font + }; + } + + internal AccessibleObject AccessibilityObject + { + get + { + if (_accessibilityObject is null) + { + _accessibilityObject = new ListViewSubItemAccessibleObject(this, owner); + } + + return _accessibilityObject; + } + } + + public Color BackColor + { + get + { + if (style != null && style.backColor != Color.Empty) + { + return style.backColor; + } + + if (owner != null && owner.listView != null) + { + return owner.listView.BackColor; + } + + return SystemColors.Window; + } + set + { + if (style is null) + { + style = new SubItemStyle(); + } + + if (style.backColor != value) + { + style.backColor = value; + owner?.InvalidateListView(); + } + } + } + [Browsable(false)] + public Rectangle Bounds + { + get + { + if (owner != null && owner.listView != null && owner.listView.IsHandleCreated) + { + return owner.listView.GetSubItemRect(owner.Index, owner.SubItems.IndexOf(this)); + } + else + { + return Rectangle.Empty; + } + } + } + + internal bool CustomBackColor + { + get + { + Debug.Assert(style != null, "Should have checked CustomStyle"); + return !style.backColor.IsEmpty; + } + } + + internal bool CustomFont + { + get + { + Debug.Assert(style != null, "Should have checked CustomStyle"); + return style.font != null; + } + } + + internal bool CustomForeColor + { + get + { + Debug.Assert(style != null, "Should have checked CustomStyle"); + return !style.foreColor.IsEmpty; + } + } + + internal bool CustomStyle => style != null; + + [Localizable(true)] + public Font Font + { + get + { + if (style != null && style.font != null) + { + return style.font; + } + + if (owner != null && owner.listView != null) + { + return owner.listView.Font; + } + + return Control.DefaultFont; + } + set + { + if (style is null) + { + style = new SubItemStyle(); + } + + if (style.font != value) + { + style.font = value; + owner?.InvalidateListView(); + } + } + } + + public Color ForeColor + { + get + { + if (style != null && style.foreColor != Color.Empty) + { + return style.foreColor; + } + + if (owner != null && owner.listView != null) + { + return owner.listView.ForeColor; + } + + return SystemColors.WindowText; + } + set + { + if (style is null) + { + style = new SubItemStyle(); + } + + if (style.foreColor != value) + { + style.foreColor = value; + owner?.InvalidateListView(); + } + } + } + + [SRCategory(nameof(SR.CatData))] + [Localizable(false)] + [Bindable(true)] + [SRDescription(nameof(SR.ControlTagDescr))] + [DefaultValue(null)] + [TypeConverter(typeof(StringConverter))] + public object Tag + { + get => userData; + set => userData = value; + } + + [Localizable(true)] + public string Text + { + get => text ?? string.Empty; + set + { + text = value; + owner?.UpdateSubItems(-1); + } + } + + [Localizable(true)] + public string Name + { + get => name ?? string.Empty; + set + { + name = value; + owner?.UpdateSubItems(-1); + } + } + + [OnDeserializing] + private void OnDeserializing(StreamingContext ctx) + { + } + + [OnDeserialized] + private void OnDeserialized(StreamingContext ctx) + { + name = null; + userData = null; + } + + [OnSerializing] + private void OnSerializing(StreamingContext ctx) + { + } + + [OnSerialized] + private void OnSerialized(StreamingContext ctx) + { + } + + public void ResetStyle() + { + if (style != null) + { + style = null; + owner?.InvalidateListView(); + } + } + + public override string ToString() => "ListViewSubItem: {" + Text + "}"; + + [Serializable] // This type is participating in resx serialization scenarios. + private class SubItemStyle + { +#pragma warning disable IDE1006 + public Color backColor = Color.Empty; // Do NOT rename (binary serialization). + public Color foreColor = Color.Empty; // Do NOT rename (binary serialization). + public Font font; // Do NOT rename (binary serialization). +#pragma warning restore IDE1006 + } + } + } +} diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/ListViewItem.ListViewSubItemCollection.cs b/src/System.Windows.Forms/src/System/Windows/Forms/ListViewItem.ListViewSubItemCollection.cs new file mode 100644 index 00000000000..621f9ffe16d --- /dev/null +++ b/src/System.Windows.Forms/src/System/Windows/Forms/ListViewItem.ListViewSubItemCollection.cs @@ -0,0 +1,451 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#nullable disable + +using System.Collections; +using System.ComponentModel; +using System.Drawing; + +namespace System.Windows.Forms +{ + public partial class ListViewItem + { + public class ListViewSubItemCollection : IList + { + private readonly ListViewItem _owner; + + // A caching mechanism for key accessor + // We use an index here rather than control so that we don't have lifetime + // issues by holding on to extra references. + private int _lastAccessedIndex = -1; + + public ListViewSubItemCollection(ListViewItem owner) + { + _owner = owner ?? throw new ArgumentNullException(nameof(owner)); + } + + /// + /// Returns the total number of items within the list view. + /// + [Browsable(false)] + public int Count => _owner.SubItemCount; + + object ICollection.SyncRoot => this; + + bool ICollection.IsSynchronized => true; + + bool IList.IsFixedSize => false; + + public bool IsReadOnly => false; + + /// + /// Returns a ListViewSubItem given it's zero based index into the ListViewSubItemCollection. + /// + public ListViewSubItem this[int index] + { + get + { + if (index < 0 || index >= Count) + { + throw new ArgumentOutOfRangeException(nameof(index), index, string.Format(SR.InvalidArgument, nameof(index), index)); + } + + return _owner.subItems[index]; + } + set + { + if (index < 0 || index >= Count) + { + throw new ArgumentOutOfRangeException(nameof(index), index, string.Format(SR.InvalidArgument, nameof(index), index)); + } + + _owner.subItems[index] = value ?? throw new ArgumentNullException(nameof(value)); + _owner.UpdateSubItems(index); + } + } + + object IList.this[int index] + { + get => this[index]; + set + { + if (!(value is ListViewSubItem item)) + { + throw new ArgumentException(SR.ListViewBadListViewSubItem, nameof(value)); + } + + this[index] = item; + } + } + /// + /// Retrieves the child control with the specified key. + /// + public virtual ListViewSubItem this[string key] + { + get + { + // We do not support null and empty string as valid keys. + if (string.IsNullOrEmpty(key)) + { + return null; + } + + // Search for the key in our collection + int index = IndexOfKey(key); + if (!IsValidIndex(index)) + { + return null; + } + + return this[index]; + } + } + + public ListViewSubItem Add(ListViewSubItem item) + { + if (item is null) + { + throw new ArgumentNullException(nameof(item)); + } + + EnsureSubItemSpace(1, -1); + item.owner = _owner; + _owner.subItems[_owner.SubItemCount] = item; + _owner.UpdateSubItems(_owner.SubItemCount++); + return item; + } + + public ListViewSubItem Add(string text) + { + ListViewSubItem item = new ListViewSubItem(_owner, text); + Add(item); + return item; + } + + public ListViewSubItem Add(string text, Color foreColor, Color backColor, Font font) + { + ListViewSubItem item = new ListViewSubItem(_owner, text, foreColor, backColor, font); + Add(item); + return item; + } + + public void AddRange(ListViewSubItem[] items) + { + if (items is null) + { + throw new ArgumentNullException(nameof(items)); + } + + EnsureSubItemSpace(items.Length, -1); + foreach (ListViewSubItem item in items) + { + if (item != null) + { + _owner.subItems[_owner.SubItemCount++] = item; + } + } + + _owner.UpdateSubItems(-1); + } + + public void AddRange(string[] items) + { + if (items is null) + { + throw new ArgumentNullException(nameof(items)); + } + + EnsureSubItemSpace(items.Length, -1); + foreach (string item in items) + { + if (item != null) + { + _owner.subItems[_owner.SubItemCount++] = new ListViewSubItem(_owner, item); + } + } + + _owner.UpdateSubItems(-1); + } + + public void AddRange(string[] items, Color foreColor, Color backColor, Font font) + { + if (items is null) + { + throw new ArgumentNullException(nameof(items)); + } + + EnsureSubItemSpace(items.Length, -1); + foreach (string item in items) + { + if (item != null) + { + _owner.subItems[_owner.SubItemCount++] = new ListViewSubItem(_owner, item, foreColor, backColor, font); + } + } + + _owner.UpdateSubItems(-1); + } + + int IList.Add(object item) + { + if (!(item is ListViewSubItem itemValue)) + { + throw new ArgumentException(SR.ListViewSubItemCollectionInvalidArgument, nameof(item)); + } + + return IndexOf(Add(itemValue)); + } + + public void Clear() + { + int oldCount = _owner.SubItemCount; + if (oldCount > 0) + { + _owner.SubItemCount = 0; + _owner.UpdateSubItems(-1, oldCount); + } + } + + public bool Contains(ListViewSubItem subItem) => IndexOf(subItem) != -1; + + bool IList.Contains(object item) + { + if (!(item is ListViewSubItem itemValue)) + { + return false; + } + + return Contains(itemValue); + } + + /// + /// Returns true if the collection contains an item with the specified key, false otherwise. + /// + public virtual bool ContainsKey(string key) => IsValidIndex(IndexOfKey(key)); + + /// + /// Ensures that the sub item array has the given + /// capacity. If it doesn't, it enlarges the + /// array until it does. If index is -1, additional + /// space is tacked onto the end. If it is a valid + /// insertion index into the array, this will move + /// the array data to accomodate the space. + /// + private void EnsureSubItemSpace(int size, int index) + { + if (_owner.SubItemCount == MaxSubItems) + { + throw new InvalidOperationException(SR.ErrorCollectionFull); + } + + if (_owner.subItems is null || _owner.SubItemCount + size > _owner.subItems.Length) + { + // Must grow array. Don't do it just by size, though; + // chunk it for efficiency. + if (_owner.subItems is null) + { + int newSize = (size > 4) ? size : 4; + _owner.subItems = new ListViewSubItem[newSize]; + } + else + { + int newSize = _owner.subItems.Length * 2; + while (newSize - _owner.SubItemCount < size) + { + newSize *= 2; + } + + ListViewSubItem[] newItems = new ListViewSubItem[newSize]; + + // Now, when copying to the member variable, use index + // if it was provided. + if (index != -1) + { + Array.Copy(_owner.subItems, 0, newItems, 0, index); + Array.Copy(_owner.subItems, index, newItems, index + size, _owner.SubItemCount - index); + } + else + { + Array.Copy(_owner.subItems, newItems, _owner.SubItemCount); + } + _owner.subItems = newItems; + } + } + else + { + // We had plenty of room. Just move the items if we need to + if (index != -1) + { + for (int i = _owner.SubItemCount - 1; i >= index; i--) + { + _owner.subItems[i + size] = _owner.subItems[i]; + } + } + } + } + + public int IndexOf(ListViewSubItem subItem) + { + for (int index = 0; index < Count; ++index) + { + if (_owner.subItems[index] == subItem) + { + return index; + } + } + + return -1; + } + + int IList.IndexOf(object subItem) + { + if (!(subItem is ListViewSubItem subItemValue)) + { + return -1; + } + + return IndexOf(subItemValue); + } + + /// + /// The zero-based index of the first occurrence of value within the entire CollectionBase, if found; otherwise, -1. + /// + public virtual int IndexOfKey(string key) + { + if (string.IsNullOrEmpty(key)) + { + return -1; + } + + if (IsValidIndex(_lastAccessedIndex)) + { + if (WindowsFormsUtils.SafeCompareStrings(this[_lastAccessedIndex].Name, key, ignoreCase: true)) + { + return _lastAccessedIndex; + } + } + + for (int i = 0; i < Count; i++) + { + if (WindowsFormsUtils.SafeCompareStrings(this[i].Name, key, ignoreCase: true)) + { + _lastAccessedIndex = i; + return i; + } + } + + _lastAccessedIndex = -1; + return -1; + } + + /// + /// Determines if the index is valid for the collection. + /// + private bool IsValidIndex(int index) => ((index >= 0) && (index < Count)); + + public void Insert(int index, ListViewSubItem item) + { + if (index < 0 || index > Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + if (item is null) + { + throw new ArgumentNullException(nameof(item)); + } + + item.owner = _owner; + + EnsureSubItemSpace(1, index); + + // Insert new item + _owner.subItems[index] = item; + _owner.SubItemCount++; + _owner.UpdateSubItems(-1); + } + + void IList.Insert(int index, object item) + { + if (!(item is ListViewSubItem itemValue)) + { + throw new ArgumentException(SR.ListViewBadListViewSubItem, nameof(item)); + } + + Insert(index, (ListViewSubItem)item); + } + + public void Remove(ListViewSubItem item) + { + int index = IndexOf(item); + if (index != -1) + { + RemoveAt(index); + } + } + + void IList.Remove(object item) + { + if (item is ListViewSubItem itemValue) + { + Remove(itemValue); + } + } + + public void RemoveAt(int index) + { + if (index < 0 || index >= Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + // Remove ourselves as the owner. + _owner.subItems[index].owner = null; + + // Collapse the items + for (int i = index + 1; i < _owner.SubItemCount; i++) + { + _owner.subItems[i - 1] = _owner.subItems[i]; + } + + int oldCount = _owner.SubItemCount; + _owner.SubItemCount--; + _owner.subItems[_owner.SubItemCount] = null; + _owner.UpdateSubItems(-1, oldCount); + } + + /// + /// Removes the child control with the specified key. + /// + public virtual void RemoveByKey(string key) + { + int index = IndexOfKey(key); + if (IsValidIndex(index)) + { + RemoveAt(index); + } + } + + void ICollection.CopyTo(Array dest, int index) + { + if (Count > 0) + { + Array.Copy(_owner.subItems, 0, dest, index, Count); + } + } + + public IEnumerator GetEnumerator() + { + if (_owner.subItems != null) + { + return new ArraySubsetEnumerator(_owner.subItems, _owner.SubItemCount); + } + else + { + return Array.Empty().GetEnumerator(); + } + } + } + } +} diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/ListViewItem.cs b/src/System.Windows.Forms/src/System/Windows/Forms/ListViewItem.cs index 989787b7981..9a354d0833c 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/ListViewItem.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/ListViewItem.cs @@ -4,7 +4,6 @@ #nullable disable -using System.Collections; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; @@ -25,7 +24,7 @@ namespace System.Windows.Forms [DesignTimeVisible(false)] [DefaultProperty(nameof(Text))] [Serializable] // This type is participating in resx serialization scenarios. - public class ListViewItem : ICloneable, ISerializable + public partial class ListViewItem : ICloneable, ISerializable { private const int MaxSubItems = 4096; @@ -57,6 +56,8 @@ public class ListViewItem : ICloneable, ISerializable private string toolTipText = string.Empty; private object userData; + private AccessibleObject _accessibilityObject; + public ListViewItem() { StateSelected = false; @@ -227,6 +228,22 @@ public ListViewItem(ListViewSubItem[] subItems, string imageKey, ListViewGroup g Group = group; } + internal AccessibleObject AccessibilityObject + { + get + { + if (_accessibilityObject is null) + { + bool inDefaultGroup = listView?.GroupsEnabled == true && Group is null; + + _accessibilityObject = new ListViewItemAccessibleObject( + this, inDefaultGroup ? listView.DefaultGroup : Group); + } + + return _accessibilityObject; + } + } + /// /// The font that this item will be displayed in. If its value is null, it will be displayed /// using the global font for the ListView control that hosts it. @@ -328,6 +345,8 @@ public bool Focused if (listView != null && listView.IsHandleCreated) { listView.SetItemState(Index, value ? LVIS.FOCUSED : 0, LVIS.FOCUSED); + + AccessibilityObject.RaiseAutomationEvent(UiaCore.UIA.AutomationFocusChangedEventId); } } } @@ -1275,700 +1294,5 @@ void ISerializable.GetObjectData(SerializationInfo info, StreamingContext contex { Serialize(info, context); } - - [TypeConverter(typeof(ListViewSubItemConverter))] - [ToolboxItem(false)] - [DesignTimeVisible(false)] - [DefaultProperty(nameof(Text))] - [Serializable] // This type is participating in resx serialization scenarios. - public class ListViewSubItem - { - [NonSerialized] - internal ListViewItem owner; -#pragma warning disable IDE1006 - private string text; // Do NOT rename (binary serialization). - - [OptionalField(VersionAdded = 2)] - private string name = null; // Do NOT rename (binary serialization). - - private SubItemStyle style; // Do NOT rename (binary serialization). - - [OptionalField(VersionAdded = 2)] - private object userData; // Do NOT rename (binary serialization). -#pragma warning restore IDE1006 - - public ListViewSubItem() - { - } - - public ListViewSubItem(ListViewItem owner, string text) - { - this.owner = owner; - this.text = text; - } - - public ListViewSubItem(ListViewItem owner, string text, Color foreColor, Color backColor, Font font) - { - this.owner = owner; - this.text = text; - style = new SubItemStyle - { - foreColor = foreColor, - backColor = backColor, - font = font - }; - } - - public Color BackColor - { - get - { - if (style != null && style.backColor != Color.Empty) - { - return style.backColor; - } - - if (owner != null && owner.listView != null) - { - return owner.listView.BackColor; - } - - return SystemColors.Window; - } - set - { - if (style is null) - { - style = new SubItemStyle(); - } - - if (style.backColor != value) - { - style.backColor = value; - owner?.InvalidateListView(); - } - } - } - [Browsable(false)] - public Rectangle Bounds - { - get - { - if (owner != null && owner.listView != null && owner.listView.IsHandleCreated) - { - return owner.listView.GetSubItemRect(owner.Index, owner.SubItems.IndexOf(this)); - } - else - { - return Rectangle.Empty; - } - } - } - - internal bool CustomBackColor - { - get - { - Debug.Assert(style != null, "Should have checked CustomStyle"); - return !style.backColor.IsEmpty; - } - } - - internal bool CustomFont - { - get - { - Debug.Assert(style != null, "Should have checked CustomStyle"); - return style.font != null; - } - } - - internal bool CustomForeColor - { - get - { - Debug.Assert(style != null, "Should have checked CustomStyle"); - return !style.foreColor.IsEmpty; - } - } - - internal bool CustomStyle => style != null; - - [Localizable(true)] - public Font Font - { - get - { - if (style != null && style.font != null) - { - return style.font; - } - - if (owner != null && owner.listView != null) - { - return owner.listView.Font; - } - - return Control.DefaultFont; - } - set - { - if (style is null) - { - style = new SubItemStyle(); - } - - if (style.font != value) - { - style.font = value; - owner?.InvalidateListView(); - } - } - } - - public Color ForeColor - { - get - { - if (style != null && style.foreColor != Color.Empty) - { - return style.foreColor; - } - - if (owner != null && owner.listView != null) - { - return owner.listView.ForeColor; - } - - return SystemColors.WindowText; - } - set - { - if (style is null) - { - style = new SubItemStyle(); - } - - if (style.foreColor != value) - { - style.foreColor = value; - owner?.InvalidateListView(); - } - } - } - - [SRCategory(nameof(SR.CatData))] - [Localizable(false)] - [Bindable(true)] - [SRDescription(nameof(SR.ControlTagDescr))] - [DefaultValue(null)] - [TypeConverter(typeof(StringConverter))] - public object Tag - { - get => userData; - set => userData = value; - } - - [Localizable(true)] - public string Text - { - get => text ?? string.Empty; - set - { - text = value; - owner?.UpdateSubItems(-1); - } - } - - [Localizable(true)] - public string Name - { - get => name ?? string.Empty; - set - { - name = value; - owner?.UpdateSubItems(-1); - } - } - - [OnDeserializing] - private void OnDeserializing(StreamingContext ctx) - { - } - - [OnDeserialized] - private void OnDeserialized(StreamingContext ctx) - { - name = null; - userData = null; - } - - [OnSerializing] - private void OnSerializing(StreamingContext ctx) - { - } - - [OnSerialized] - private void OnSerialized(StreamingContext ctx) - { - } - - public void ResetStyle() - { - if (style != null) - { - style = null; - owner?.InvalidateListView(); - } - } - - public override string ToString() => "ListViewSubItem: {" + Text + "}"; - - [Serializable] // This type is participating in resx serialization scenarios. - private class SubItemStyle - { -#pragma warning disable IDE1006 - public Color backColor = Color.Empty; // Do NOT rename (binary serialization). - public Color foreColor = Color.Empty; // Do NOT rename (binary serialization). - public Font font; // Do NOT rename (binary serialization). -#pragma warning restore IDE1006 - } - } - - public class ListViewSubItemCollection : IList - { - private readonly ListViewItem _owner; - - // A caching mechanism for key accessor - // We use an index here rather than control so that we don't have lifetime - // issues by holding on to extra references. - private int _lastAccessedIndex = -1; - - public ListViewSubItemCollection(ListViewItem owner) - { - _owner = owner ?? throw new ArgumentNullException(nameof(owner)); - } - - /// - /// Returns the total number of items within the list view. - /// - [Browsable(false)] - public int Count => _owner.SubItemCount; - - object ICollection.SyncRoot => this; - - bool ICollection.IsSynchronized => true; - - bool IList.IsFixedSize => false; - - public bool IsReadOnly => false; - - /// - /// Returns a ListViewSubItem given it's zero based index into the ListViewSubItemCollection. - /// - public ListViewSubItem this[int index] - { - get - { - if (index < 0 || index >= Count) - { - throw new ArgumentOutOfRangeException(nameof(index), index, string.Format(SR.InvalidArgument, nameof(index), index)); - } - - return _owner.subItems[index]; - } - set - { - if (index < 0 || index >= Count) - { - throw new ArgumentOutOfRangeException(nameof(index), index, string.Format(SR.InvalidArgument, nameof(index), index)); - } - - _owner.subItems[index] = value ?? throw new ArgumentNullException(nameof(value)); - _owner.UpdateSubItems(index); - } - } - - object IList.this[int index] - { - get => this[index]; - set - { - if (!(value is ListViewSubItem item)) - { - throw new ArgumentException(SR.ListViewBadListViewSubItem, nameof(value)); - } - - this[index] = item; - } - } - /// - /// Retrieves the child control with the specified key. - /// - public virtual ListViewSubItem this[string key] - { - get - { - // We do not support null and empty string as valid keys. - if (string.IsNullOrEmpty(key)) - { - return null; - } - - // Search for the key in our collection - int index = IndexOfKey(key); - if (!IsValidIndex(index)) - { - return null; - } - - return this[index]; - } - } - - public ListViewSubItem Add(ListViewSubItem item) - { - if (item is null) - { - throw new ArgumentNullException(nameof(item)); - } - - EnsureSubItemSpace(1, -1); - item.owner = _owner; - _owner.subItems[_owner.SubItemCount] = item; - _owner.UpdateSubItems(_owner.SubItemCount++); - return item; - } - - public ListViewSubItem Add(string text) - { - ListViewSubItem item = new ListViewSubItem(_owner, text); - Add(item); - return item; - } - - public ListViewSubItem Add(string text, Color foreColor, Color backColor, Font font) - { - ListViewSubItem item = new ListViewSubItem(_owner, text, foreColor, backColor, font); - Add(item); - return item; - } - - public void AddRange(ListViewSubItem[] items) - { - if (items is null) - { - throw new ArgumentNullException(nameof(items)); - } - - EnsureSubItemSpace(items.Length, -1); - foreach (ListViewSubItem item in items) - { - if (item != null) - { - _owner.subItems[_owner.SubItemCount++] = item; - } - } - - _owner.UpdateSubItems(-1); - } - - public void AddRange(string[] items) - { - if (items is null) - { - throw new ArgumentNullException(nameof(items)); - } - - EnsureSubItemSpace(items.Length, -1); - foreach (string item in items) - { - if (item != null) - { - _owner.subItems[_owner.SubItemCount++] = new ListViewSubItem(_owner, item); - } - } - - _owner.UpdateSubItems(-1); - } - - public void AddRange(string[] items, Color foreColor, Color backColor, Font font) - { - if (items is null) - { - throw new ArgumentNullException(nameof(items)); - } - - EnsureSubItemSpace(items.Length, -1); - foreach (string item in items) - { - if (item != null) - { - _owner.subItems[_owner.SubItemCount++] = new ListViewSubItem(_owner, item, foreColor, backColor, font); - } - } - - _owner.UpdateSubItems(-1); - } - - int IList.Add(object item) - { - if (!(item is ListViewSubItem itemValue)) - { - throw new ArgumentException(SR.ListViewSubItemCollectionInvalidArgument, nameof(item)); - } - - return IndexOf(Add(itemValue)); - } - - public void Clear() - { - int oldCount = _owner.SubItemCount; - if (oldCount > 0) - { - _owner.SubItemCount = 0; - _owner.UpdateSubItems(-1, oldCount); - } - } - - public bool Contains(ListViewSubItem subItem) => IndexOf(subItem) != -1; - - bool IList.Contains(object item) - { - if (!(item is ListViewSubItem itemValue)) - { - return false; - } - - return Contains(itemValue); - } - - /// - /// Returns true if the collection contains an item with the specified key, false otherwise. - /// - public virtual bool ContainsKey(string key) => IsValidIndex(IndexOfKey(key)); - - /// - /// Ensures that the sub item array has the given - /// capacity. If it doesn't, it enlarges the - /// array until it does. If index is -1, additional - /// space is tacked onto the end. If it is a valid - /// insertion index into the array, this will move - /// the array data to accomodate the space. - /// - private void EnsureSubItemSpace(int size, int index) - { - if (_owner.SubItemCount == MaxSubItems) - { - throw new InvalidOperationException(SR.ErrorCollectionFull); - } - - if (_owner.subItems is null || _owner.SubItemCount + size > _owner.subItems.Length) - { - // Must grow array. Don't do it just by size, though; - // chunk it for efficiency. - if (_owner.subItems is null) - { - int newSize = (size > 4) ? size : 4; - _owner.subItems = new ListViewSubItem[newSize]; - } - else - { - int newSize = _owner.subItems.Length * 2; - while (newSize - _owner.SubItemCount < size) - { - newSize *= 2; - } - - ListViewSubItem[] newItems = new ListViewSubItem[newSize]; - - // Now, when copying to the member variable, use index - // if it was provided. - if (index != -1) - { - Array.Copy(_owner.subItems, 0, newItems, 0, index); - Array.Copy(_owner.subItems, index, newItems, index + size, _owner.SubItemCount - index); - } - else - { - Array.Copy(_owner.subItems, newItems, _owner.SubItemCount); - } - _owner.subItems = newItems; - } - } - else - { - // We had plenty of room. Just move the items if we need to - if (index != -1) - { - for (int i = _owner.SubItemCount - 1; i >= index; i--) - { - _owner.subItems[i + size] = _owner.subItems[i]; - } - } - } - } - - public int IndexOf(ListViewSubItem subItem) - { - for (int index = 0; index < Count; ++index) - { - if (_owner.subItems[index] == subItem) - { - return index; - } - } - - return -1; - } - - int IList.IndexOf(object subItem) - { - if (!(subItem is ListViewSubItem subItemValue)) - { - return -1; - } - - return IndexOf(subItemValue); - } - - /// - /// The zero-based index of the first occurrence of value within the entire CollectionBase, if found; otherwise, -1. - /// - public virtual int IndexOfKey(string key) - { - if (string.IsNullOrEmpty(key)) - { - return -1; - } - - if (IsValidIndex(_lastAccessedIndex)) - { - if (WindowsFormsUtils.SafeCompareStrings(this[_lastAccessedIndex].Name, key, ignoreCase: true)) - { - return _lastAccessedIndex; - } - } - - for (int i = 0; i < Count; i++) - { - if (WindowsFormsUtils.SafeCompareStrings(this[i].Name, key, ignoreCase: true)) - { - _lastAccessedIndex = i; - return i; - } - } - - _lastAccessedIndex = -1; - return -1; - } - - /// - /// Determines if the index is valid for the collection. - /// - private bool IsValidIndex(int index) => ((index >= 0) && (index < Count)); - - public void Insert(int index, ListViewSubItem item) - { - if (index < 0 || index > Count) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - if (item is null) - { - throw new ArgumentNullException(nameof(item)); - } - - item.owner = _owner; - - EnsureSubItemSpace(1, index); - - // Insert new item - _owner.subItems[index] = item; - _owner.SubItemCount++; - _owner.UpdateSubItems(-1); - } - - void IList.Insert(int index, object item) - { - if (!(item is ListViewSubItem itemValue)) - { - throw new ArgumentException(SR.ListViewBadListViewSubItem, nameof(item)); - } - - Insert(index, (ListViewSubItem)item); - } - - public void Remove(ListViewSubItem item) - { - int index = IndexOf(item); - if (index != -1) - { - RemoveAt(index); - } - } - - void IList.Remove(object item) - { - if (item is ListViewSubItem itemValue) - { - Remove(itemValue); - } - } - - public void RemoveAt(int index) - { - if (index < 0 || index >= Count) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - // Remove ourselves as the owner. - _owner.subItems[index].owner = null; - - // Collapse the items - for (int i = index + 1; i < _owner.SubItemCount; i++) - { - _owner.subItems[i - 1] = _owner.subItems[i]; - } - - int oldCount = _owner.SubItemCount; - _owner.SubItemCount--; - _owner.subItems[_owner.SubItemCount] = null; - _owner.UpdateSubItems(-1, oldCount); - } - - /// - /// Removes the child control with the specified key. - /// - public virtual void RemoveByKey(string key) - { - int index = IndexOfKey(key); - if (IsValidIndex(index)) - { - RemoveAt(index); - } - } - - void ICollection.CopyTo(Array dest, int index) - { - if (Count > 0) - { - Array.Copy(_owner.subItems, 0, dest, index, Count); - } - } - - public IEnumerator GetEnumerator() - { - if (_owner.subItems != null) - { - return new ArraySubsetEnumerator(_owner.subItems, _owner.SubItemCount); - } - else - { - return Array.Empty().GetEnumerator(); - } - } - } } } diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ListVIew.ListViewAccessibleObjectTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ListVIew.ListViewAccessibleObjectTests.cs new file mode 100644 index 00000000000..6426909b2f9 --- /dev/null +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ListVIew.ListViewAccessibleObjectTests.cs @@ -0,0 +1,180 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Xunit; +using static System.Windows.Forms.ListViewGroup; +using static System.Windows.Forms.ListViewItem; +using static Interop; + +namespace System.Windows.Forms.Tests +{ + public class ListView_ListViewAccessibleObjectTests + { + [WinFormsFact] + public void ListViewAccessibleObject_Ctor_Default() + { + using ListView listView = new ListView(); + + AccessibleObject accessibleObject = listView.AccessibilityObject; + Assert.True(listView.IsHandleCreated); + Assert.NotNull(accessibleObject); + Assert.Equal(AccessibleRole.List, accessibleObject.Role); + } + + [WinFormsFact] + public void ListViewAccessibleObject_EmptyList_GetChildCount_ReturnsCorrectValue() + { + using ListView listView = new ListView(); + AccessibleObject accessibleObject = listView.AccessibilityObject; + Assert.True(listView.IsHandleCreated); + Assert.Equal(0, accessibleObject.GetChildCount()); // listView doesn't have items + } + + [WinFormsFact] + public void ListViewAccessibleObject_GetMultiViewProviderCurrentView_ReturnsCorrectValue() + { + using ListView listView = new ListView(); + AccessibleObject accessibleObject = listView.AccessibilityObject; + Assert.True(listView.IsHandleCreated); + Assert.Equal((int)listView.View, accessibleObject.GetMultiViewProviderCurrentView()); + } + + [WinFormsFact] + public void ListViewAccessibleObject_GetMultiViewProviderSupportedViews_ReturnsExpected() + { + using ListView listView = new ListView(); + AccessibleObject accessibleObject = listView.AccessibilityObject; + Assert.True(listView.IsHandleCreated); + Assert.Equal(new int[] { (int)View.Details }, accessibleObject.GetMultiViewProviderSupportedViews()); + } + + [WinFormsFact] + public void ListViewAccessibleObject_GetMultiViewProviderViewName_ReturnsCorrectValue() + { + using ListView listView = new ListView(); + listView.View = View.Details; + AccessibleObject accessibleObject = listView.AccessibilityObject; + Assert.True(listView.IsHandleCreated); + Assert.Equal(((int)(listView.View)).ToString(), accessibleObject.GetMultiViewProviderViewName((int)View.Details)); + } + + [WinFormsFact] + public void ListViewAccessibleObject_ListWithOneItem_GetChildCount_ReturnsCorrectValue() + { + using ListView listView = new ListView(); + AccessibleObject accessibleObject = listView.AccessibilityObject; + Assert.True(listView.IsHandleCreated); + listView.Items.Add(new ListViewItem()); + Assert.Equal(1, accessibleObject.GetChildCount()); // One item + } + + [WinFormsFact] + public void ListViewAccessibleObject_ListWithTwoGroups_GetChildCount_ReturnsCorrectValue() + { + using ListView listView = new ListView(); + listView.Items.Add(new ListViewItem()); + ListViewItem item = new ListViewItem(); + ListViewItem item2 = new ListViewItem(); + ListViewGroup group = new ListViewGroup(); + item2.Group = group; + item.Group = group; + listView.Groups.Add(group); + listView.Items.Add(item); + listView.Items.Add(item2); + + AccessibleObject accessibleObject = listView.AccessibilityObject; + Assert.True(listView.IsHandleCreated); + Assert.Equal(2, accessibleObject.GetChildCount()); // Default group and one specified group + } + + [WinFormsFact] + public void ListViewAccessibleObject_ListWithTwoGroups_FragmentNavigateWorkCorrectly() + { + using ListView listView = new ListView(); + listView.Items.Add(new ListViewItem()); + ListViewItem item = new ListViewItem(); + ListViewItem item2 = new ListViewItem(); + ListViewGroup group = new ListViewGroup(); + item2.Group = group; + item.Group = group; + listView.Groups.Add(group); + listView.Items.Add(item); + listView.Items.Add(item2); + + AccessibleObject accessibleObject = listView.AccessibilityObject; + Assert.True(listView.IsHandleCreated); + + AccessibleObject firstChild = accessibleObject.FragmentNavigate(UiaCore.NavigateDirection.FirstChild) as AccessibleObject; + AccessibleObject lastChild = accessibleObject.FragmentNavigate(UiaCore.NavigateDirection.LastChild) as AccessibleObject; + Assert.IsType(firstChild); + Assert.IsType(lastChild); + Assert.NotEqual(firstChild, lastChild); + } + + [WinFormsFact] + public void ListViewAccessibleObject_ListWithTwoItems_GetChildCount_ReturnsCorrectValue() + { + using ListView listView = new ListView(); + listView.Items.Add(new ListViewItem()); + listView.Items.Add(new ListViewItem()); + + AccessibleObject accessibleObject = listView.AccessibilityObject; + Assert.True(listView.IsHandleCreated); + Assert.Equal(2, accessibleObject.GetChildCount()); // Two child items + } + + [WinFormsFact] + public void ListViewAccessibleObject_ListWithTwoItems_FragmentNavigateWorkCorrectly() + { + using ListView listView = new ListView(); + listView.Items.Add(new ListViewItem()); + listView.Items.Add(new ListViewItem()); + + AccessibleObject accessibleObject = listView.AccessibilityObject; + Assert.True(listView.IsHandleCreated); + + AccessibleObject firstChild = accessibleObject.FragmentNavigate(UiaCore.NavigateDirection.FirstChild) as AccessibleObject; + AccessibleObject lastChild = accessibleObject.FragmentNavigate(UiaCore.NavigateDirection.LastChild) as AccessibleObject; + Assert.IsType(firstChild); + Assert.IsType(lastChild); + Assert.NotEqual(firstChild, lastChild); + } + + [WinFormsFact] + public void ListViewAccessibleObject_EmptyList_GetChild_ReturnsCorrectValue() + { + using ListView listView = new ListView(); + AccessibleObject accessibleObject = listView.AccessibilityObject; + Assert.True(listView.IsHandleCreated); + Assert.Equal(0, accessibleObject.GetChildCount()); // listView doesn't have items + Assert.Null(accessibleObject.GetChild(0)); // GetChild method should not throw an exception + } + + [WinFormsFact] + public void ListViewAccessibleObject_GetPropertyValue_returns_correct_values() + { + using var list = new ListView(); + list.Name = "List"; + list.AccessibleName = "ListView"; + AccessibleObject listAccessibleObject = list.AccessibilityObject; + Assert.True(list.IsHandleCreated); + + object accessibleName = listAccessibleObject.GetPropertyValue(UiaCore.UIA.NamePropertyId); + Assert.Equal("ListView", accessibleName); + + object automationId = listAccessibleObject.GetPropertyValue(UiaCore.UIA.AutomationIdPropertyId); + Assert.Equal("List", automationId); + + object accessibleControlType = listAccessibleObject.GetPropertyValue(UiaCore.UIA.ControlTypePropertyId); + Assert.Equal(UiaCore.UIA.ListControlTypeId, accessibleControlType); + + object controlType = listAccessibleObject.GetPropertyValue(UiaCore.UIA.ControlTypePropertyId); + UiaCore.UIA expected = UiaCore.UIA.ListControlTypeId; + Assert.Equal(expected, controlType); + + Assert.True((bool)listAccessibleObject.GetPropertyValue(UiaCore.UIA.IsMultipleViewPatternAvailablePropertyId)); + } + } +} + diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ListViewGroup.ListViewGroupAccessibleObjectTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ListViewGroup.ListViewGroupAccessibleObjectTests.cs new file mode 100644 index 00000000000..d20681818b1 --- /dev/null +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ListViewGroup.ListViewGroupAccessibleObjectTests.cs @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Drawing; +using System.Reflection; +using System.Threading; +using Microsoft.DotNet.RemoteExecutor; +using Xunit; +using static System.Windows.Forms.ListViewGroup; +using static System.Windows.Forms.ListViewItem; +using static Interop; + +namespace System.Windows.Forms.Tests +{ + public class ListViewGroup_ListViewGroupAccessibleObjectTests + { + [WinFormsFact] + public void ListViewGroupAccessibleObject_Ctor_ThrowsArgumentNullException() + { + using ListView list = new ListView(); + ListViewGroup listGroup = new ListViewGroup("Group1"); + listGroup.Items.Add(new ListViewItem()); + list.Groups.Add(listGroup); + + Type type = listGroup.AccessibilityObject.GetType(); + ConstructorInfo ctor = type.GetConstructor(new Type[] { typeof(ListViewGroup), typeof(bool) }); + Assert.NotNull(ctor); + Assert.Throws(() => ctor.Invoke(new object[] { null, false })); + + // group without parent ListView + ListViewGroup listGroupWithoutList = new ListViewGroup("Group2"); + Assert.Throws(() => ctor.Invoke(new object[] { listGroupWithoutList, false })); + } + + [WinFormsFact] + public void ListViewGroupAccessibleObject_Ctor_Default() + { + using ListView list = new ListView(); + ListViewGroup listGroup = new ListViewGroup("Group1"); + listGroup.Items.Add(new ListViewItem()); + list.Groups.Add(listGroup); + + AccessibleObject accessibleObject = listGroup.AccessibilityObject; + Assert.False(list.IsHandleCreated); + + Assert.NotNull(accessibleObject); + Assert.Equal(AccessibleRole.Grouping, accessibleObject.Role); + } + + [WinFormsFact] + public void ListViewGroupAccessibleObject_GetPropertyValue_returns_correct_values() + { + using ListView list = new ListView(); + ListViewGroup listGroup = new ListViewGroup("Group1"); + listGroup.Items.Add(new ListViewItem()); + list.Groups.Add(listGroup); + + AccessibleObject accessibleObject = listGroup.AccessibilityObject; + Assert.False(list.IsHandleCreated); + + object accessibleName = accessibleObject.GetPropertyValue(UiaCore.UIA.NamePropertyId); + Assert.Equal("Group1", accessibleName); + + object automationId = accessibleObject.GetPropertyValue(UiaCore.UIA.AutomationIdPropertyId); + Assert.Equal("ListViewGroup-0", automationId); + + object controlType = accessibleObject.GetPropertyValue(UiaCore.UIA.ControlTypePropertyId); + UiaCore.UIA expected = UiaCore.UIA.GroupControlTypeId; + Assert.Equal(expected, controlType); + + Assert.True((bool)accessibleObject.GetPropertyValue(UiaCore.UIA.IsLegacyIAccessiblePatternAvailablePropertyId)); + } + + [WinFormsFact] + public void ListViewGroupAccessibleObject_ListWithTwoGroups_FragmentNavigateWorkCorrectly() + { + using ListView list = new ListView(); + ListViewGroup listGroup = new ListViewGroup("Group1"); + ListViewItem listItem1 = new ListViewItem(); + ListViewItem listItem2 = new ListViewItem(); + ListViewItem listItem3 = new ListViewItem(); + list.Groups.Add(listGroup); + listItem1.Group = listGroup; + listItem2.Group = listGroup; + list.Items.Add(listItem1); + list.Items.Add(listItem2); + list.Items.Add(listItem3); + AccessibleObject group1AccObj = listGroup.AccessibilityObject; + AccessibleObject defaultGroupAccObj = list.DefaultGroup.AccessibilityObject; + Assert.False(list.IsHandleCreated); + + // Next/Previous siblings test + Assert.Null(defaultGroupAccObj.FragmentNavigate(UiaCore.NavigateDirection.PreviousSibling)); + UiaCore.IRawElementProviderFragment defaultGroupNextSibling = defaultGroupAccObj.FragmentNavigate(UiaCore.NavigateDirection.NextSibling); + Assert.IsType(group1AccObj); + Assert.Equal(group1AccObj, defaultGroupNextSibling); + + Assert.Null(group1AccObj.FragmentNavigate(UiaCore.NavigateDirection.NextSibling)); + UiaCore.IRawElementProviderFragment group1PreviousSibling = group1AccObj.FragmentNavigate(UiaCore.NavigateDirection.PreviousSibling); + Assert.IsType(group1PreviousSibling); + Assert.Equal(defaultGroupAccObj, group1PreviousSibling); + + // Parent + Assert.Equal(defaultGroupAccObj.FragmentNavigate(UiaCore.NavigateDirection.Parent), list.AccessibilityObject); + Assert.Equal(group1AccObj.FragmentNavigate(UiaCore.NavigateDirection.Parent), list.AccessibilityObject); + + // Childs + AccessibleObject firstChild = group1AccObj.FragmentNavigate(UiaCore.NavigateDirection.FirstChild) as AccessibleObject; + AccessibleObject lastChild = group1AccObj.FragmentNavigate(UiaCore.NavigateDirection.LastChild) as AccessibleObject; + Assert.IsType(firstChild); + Assert.IsType(lastChild); + Assert.NotEqual(firstChild, lastChild); + Assert.Equal(firstChild, listItem1.AccessibilityObject); + Assert.Equal(lastChild, listItem2.AccessibilityObject); + + firstChild = defaultGroupAccObj.FragmentNavigate(UiaCore.NavigateDirection.FirstChild) as AccessibleObject; + lastChild = defaultGroupAccObj.FragmentNavigate(UiaCore.NavigateDirection.LastChild) as AccessibleObject; + Assert.IsType(firstChild); + Assert.IsType(lastChild); + Assert.Equal(firstChild, lastChild); + Assert.Equal(firstChild, listItem3.AccessibilityObject); + } + + [WinFormsFact] + public void ListViewGroupAccessibleObject_Bounds_ReturnsCorrectValue() + { + using RemoteInvokeHandle invokerHandle = RemoteExecutor.Invoke(() => + { + Control.CheckForIllegalCrossThreadCalls = true; + using Form form = new Form(); + + using ListView list = new ListView(); + ListViewGroup listGroup = new ListViewGroup("Group1"); + ListViewItem listItem1 = new ListViewItem("Item1"); + ListViewItem listItem2 = new ListViewItem("Item2"); + list.Groups.Add(listGroup); + listItem1.Group = listGroup; + listItem2.Group = listGroup; + list.Items.Add(listItem1); + list.Items.Add(listItem2); + list.CreateControl(); + form.Controls.Add(list); + form.Show(); + + AccessibleObject accessibleObject = list.AccessibilityObject; + AccessibleObject group1AccObj = listGroup.AccessibilityObject; + Assert.True(list.IsHandleCreated); + + RECT groupRect = new RECT(); + User32.SendMessageW(list, (User32.WM)ComCtl32.LVM.GETGROUPRECT, (IntPtr)list.Groups.IndexOf(listGroup), ref groupRect); + + int actualWidth = group1AccObj.Bounds.Width; + int expectedWidth = groupRect.Width; + Assert.Equal(expectedWidth, actualWidth); + + int actualHeight = group1AccObj.Bounds.Height; + int expectedHeight = groupRect.Height; + Assert.Equal(expectedHeight, actualHeight); + + Rectangle actualBounds = group1AccObj.Bounds; + actualBounds.Location = new Point(0, 0); + Rectangle expectedBounds = groupRect; + Assert.Equal(expectedBounds, actualBounds); + }); + } + } +} diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ListViewItem.ListViewItemAccessibleObjectTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ListViewItem.ListViewItemAccessibleObjectTests.cs new file mode 100644 index 00000000000..9f91dab38b0 --- /dev/null +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ListViewItem.ListViewItemAccessibleObjectTests.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Reflection; +using Xunit; +using static System.Windows.Forms.ListViewItem; +using static System.Windows.Forms.ListViewItem.ListViewSubItem; +using static Interop; + +namespace System.Windows.Forms.Tests +{ + public class ListViewItem_ListViewItemAccessibleObjectTests + { + [WinFormsFact] + public void ListViewItemAccessibleObject_Ctor_ThrowsArgumentNullException() + { + using ListView list = new ListView(); + ListViewItem listItem = new ListViewItem(); + list.Items.Add(listItem); + + Type type = listItem.AccessibilityObject.GetType(); + ConstructorInfo ctor = type.GetConstructor(new Type[] { typeof(ListViewItem), typeof(ListViewGroup) }); + Assert.NotNull(ctor); + Assert.Throws(() => ctor.Invoke(new object[] { null, null })); + + // item without parent ListView + ListViewItem itemWithoutList = new ListViewItem(); + Assert.Throws(() => ctor.Invoke(new object[] { itemWithoutList, null })); + } + + [WinFormsFact] + public void ListViewItemAccessibleObject_Ctor_Default() + { + using ListView list = new ListView(); + ListViewItem listItem = new ListViewItem(); + list.Items.Add(listItem); + + AccessibleObject accessibleObject = listItem.AccessibilityObject; + Assert.True(list.IsHandleCreated); + Assert.NotNull(accessibleObject); + Assert.Equal(AccessibleRole.ListItem, accessibleObject.Role); + } + + [WinFormsFact] + public void ListViewItemAccessibleObject_GetPropertyValue_returns_correct_values() + { + using var list = new ListView(); + ListViewItem listItem = new ListViewItem("ListItem"); + list.Items.Add(listItem); + AccessibleObject listItemAccessibleObject = listItem.AccessibilityObject; + Assert.True(list.IsHandleCreated); + + object accessibleName = listItemAccessibleObject.GetPropertyValue(UiaCore.UIA.NamePropertyId); + Assert.Equal("ListItem", accessibleName); + + object automationId = listItemAccessibleObject.GetPropertyValue(UiaCore.UIA.AutomationIdPropertyId); + Assert.Equal("ListViewItem-0", automationId); + + object controlType = listItemAccessibleObject.GetPropertyValue(UiaCore.UIA.ControlTypePropertyId); + UiaCore.UIA expected = UiaCore.UIA.ListItemControlTypeId; + Assert.Equal(expected, controlType); + + Assert.True((bool)listItemAccessibleObject.GetPropertyValue(UiaCore.UIA.IsSelectionItemPatternAvailablePropertyId)); + Assert.True((bool)listItemAccessibleObject.GetPropertyValue(UiaCore.UIA.IsScrollItemPatternAvailablePropertyId)); + Assert.True((bool)listItemAccessibleObject.GetPropertyValue(UiaCore.UIA.IsInvokePatternAvailablePropertyId)); + } + + [WinFormsFact] + public void ListViewItemAccessibleObject_ListWithTwoItems_FragmentNavigateWorkCorrectly() + { + using ListView listView = new ListView(); + ListViewItem listItem1 = new ListViewItem(new string[] { + "Test A", + "Alpha"}, -1); + ListViewItem listItem2 = new ListViewItem(new string[] { + "Test B", + "Beta"}, -1); + ListViewItem listItem3 = new ListViewItem(new string[] { + "Test C", + "Gamma"}, -1); + listView.Items.Add(listItem1); + listView.Items.Add(listItem2); + listView.Items.Add(listItem3); + AccessibleObject accessibleObject1 = listItem1.AccessibilityObject; + AccessibleObject accessibleObject2 = listItem2.AccessibilityObject; + AccessibleObject accessibleObject3 = listItem3.AccessibilityObject; + Assert.True(listView.IsHandleCreated); + + // First list view item + Assert.Null(accessibleObject1.FragmentNavigate(UiaCore.NavigateDirection.PreviousSibling)); + UiaCore.IRawElementProviderFragment listItem1NextSibling = accessibleObject1.FragmentNavigate(UiaCore.NavigateDirection.NextSibling); + Assert.IsType(listItem1NextSibling); + Assert.Equal(accessibleObject2, listItem1NextSibling); + + // Second list view item + UiaCore.IRawElementProviderFragment listItem2PreviousSibling = accessibleObject2.FragmentNavigate(UiaCore.NavigateDirection.PreviousSibling); + UiaCore.IRawElementProviderFragment listItem2NextSibling = accessibleObject2.FragmentNavigate(UiaCore.NavigateDirection.NextSibling); + Assert.IsType(listItem2PreviousSibling); + Assert.IsType(listItem2NextSibling); + Assert.Equal(accessibleObject1, listItem2PreviousSibling); + Assert.Equal(accessibleObject3, listItem2NextSibling); + + // Third list view item + Assert.Null(accessibleObject3.FragmentNavigate(UiaCore.NavigateDirection.NextSibling)); + UiaCore.IRawElementProviderFragment listItem3PreviousSibling = accessibleObject3.FragmentNavigate(UiaCore.NavigateDirection.PreviousSibling); + Assert.IsType(listItem3PreviousSibling); + Assert.Equal(accessibleObject2, listItem3PreviousSibling); + + // Parent + Assert.Equal(accessibleObject1.FragmentNavigate(UiaCore.NavigateDirection.Parent), listView.AccessibilityObject); + Assert.Equal(accessibleObject2.FragmentNavigate(UiaCore.NavigateDirection.Parent), listView.AccessibilityObject); + Assert.Equal(accessibleObject3.FragmentNavigate(UiaCore.NavigateDirection.Parent), listView.AccessibilityObject); + + // Childs + AccessibleObject firstChild = accessibleObject1.FragmentNavigate(UiaCore.NavigateDirection.FirstChild) as AccessibleObject; + AccessibleObject lastChild = accessibleObject1.FragmentNavigate(UiaCore.NavigateDirection.LastChild) as AccessibleObject; + Assert.IsType(firstChild); + Assert.IsType(lastChild); + Assert.NotEqual(firstChild, lastChild); + } + } +} diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ListViewItem.ListViewSubItem.ListViewSubItemAccessibleObjectTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ListViewItem.ListViewSubItem.ListViewSubItemAccessibleObjectTests.cs new file mode 100644 index 00000000000..365bbdcb4d9 --- /dev/null +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ListViewItem.ListViewSubItem.ListViewSubItemAccessibleObjectTests.cs @@ -0,0 +1,233 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Drawing; +using Xunit; +using static System.Windows.Forms.ListViewItem.ListViewSubItem; +using static Interop; + +namespace System.Windows.Forms.Tests +{ + public class ListViewItem_ListViewSubItem_ListViewSubItemAccessibleObjectTests + { + [WinFormsFact] + public void ListViewSubItemAccessibleObject_GetChild_ReturnCorrectValue() + { + using ListView list = new ListView(); + ListViewItem listViewItem1 = new ListViewItem(new string[] { + "Test 1", + "Item 1", + "Something 1"}, -1); + + ColumnHeader columnHeader1 = new ColumnHeader(); + ColumnHeader columnHeader2 = new ColumnHeader(); + ColumnHeader columnHeader3 = new ColumnHeader(); + + list.Columns.AddRange(new ColumnHeader[] { + columnHeader1, + columnHeader2, + columnHeader3}); + list.HideSelection = false; + list.Items.Add(listViewItem1); + list.View = View.Details; + + AccessibleObject accessibleObject = listViewItem1.AccessibilityObject.GetChild(0); + Assert.True(list.IsHandleCreated); + Assert.NotNull(accessibleObject); + Assert.IsType(accessibleObject); + } + + [WinFormsFact] + public void ListViewSubItemAccessibleObject_GetPropertyValue_returns_correct_values() + { + using ListView list = new ListView(); + ListViewItem listViewItem1 = new ListViewItem(new string[] { + "Test 1", + "Item 1", + "Something 1"}, -1); + + ColumnHeader columnHeader1 = new ColumnHeader(); + ColumnHeader columnHeader2 = new ColumnHeader(); + ColumnHeader columnHeader3 = new ColumnHeader(); + + list.Columns.AddRange(new ColumnHeader[] { + columnHeader1, + columnHeader2, + columnHeader3}); + list.HideSelection = false; + list.Items.Add(listViewItem1); + list.View = View.Details; + + AccessibleObject accessibleObject = listViewItem1.AccessibilityObject.GetChild(0); + Assert.True(list.IsHandleCreated); + + object accessibleName = accessibleObject.GetPropertyValue(UiaCore.UIA.NamePropertyId); + Assert.Equal("Test 1", accessibleName); + + object automationId = accessibleObject.GetPropertyValue(UiaCore.UIA.AutomationIdPropertyId); + Assert.Equal("ListViewSubItem-0", automationId); + + object frameworkId = accessibleObject.GetPropertyValue(UiaCore.UIA.FrameworkIdPropertyId); + Assert.Equal("WinForm", frameworkId); + + object controlType = accessibleObject.GetPropertyValue(UiaCore.UIA.ControlTypePropertyId); + UiaCore.UIA expected = UiaCore.UIA.TextControlTypeId; + Assert.Equal(expected, controlType); + + Assert.True((bool)accessibleObject.GetPropertyValue(UiaCore.UIA.IsGridItemPatternAvailablePropertyId)); + Assert.True((bool)accessibleObject.GetPropertyValue(UiaCore.UIA.IsTableItemPatternAvailablePropertyId)); + } + + [WinFormsFact] + public void ListViewSubItemAccessibleObject_FragmentNavigate_WorkCorrectly() + { + using ListView list = new ListView(); + ListViewItem listViewItem1 = new ListViewItem(new string[] { + "Test 1", + "Item 1", + "Something 1"}, -1); + + ColumnHeader columnHeader1 = new ColumnHeader(); + ColumnHeader columnHeader2 = new ColumnHeader(); + ColumnHeader columnHeader3 = new ColumnHeader(); + + list.Columns.AddRange(new ColumnHeader[] { + columnHeader1, + columnHeader2, + columnHeader3}); + list.HideSelection = false; + list.Items.Add(listViewItem1); + list.View = View.Details; + + AccessibleObject subItemAccObj1 = listViewItem1.AccessibilityObject.GetChild(0); + AccessibleObject subItemAccObj2 = listViewItem1.AccessibilityObject.GetChild(1); + AccessibleObject subItemAccObj3 = listViewItem1.AccessibilityObject.GetChild(2); + Assert.True(list.IsHandleCreated); + + Assert.Null(subItemAccObj1.FragmentNavigate(UiaCore.NavigateDirection.PreviousSibling)); + UiaCore.IRawElementProviderFragment subItem1NextSibling = subItemAccObj1.FragmentNavigate(UiaCore.NavigateDirection.NextSibling); + Assert.IsType(subItem1NextSibling); + Assert.NotNull(subItem1NextSibling); + + UiaCore.IRawElementProviderFragment subItem2NextSibling = subItemAccObj2.FragmentNavigate(UiaCore.NavigateDirection.NextSibling); + UiaCore.IRawElementProviderFragment subItem2PreviousSibling = subItemAccObj2.FragmentNavigate(UiaCore.NavigateDirection.PreviousSibling); + Assert.IsType(subItem2NextSibling); + Assert.IsType(subItem2PreviousSibling); + Assert.NotNull(subItem2NextSibling); + Assert.NotNull(subItem2PreviousSibling); + + Assert.Null(subItemAccObj3.FragmentNavigate(UiaCore.NavigateDirection.NextSibling)); + UiaCore.IRawElementProviderFragment subItem3PreviousSibling = subItemAccObj3.FragmentNavigate(UiaCore.NavigateDirection.PreviousSibling); + Assert.IsType(subItem3PreviousSibling); + Assert.NotNull(subItem3PreviousSibling); + + // Parent + Assert.Equal(subItemAccObj1.FragmentNavigate(UiaCore.NavigateDirection.Parent), listViewItem1.AccessibilityObject); + Assert.Equal(subItemAccObj2.FragmentNavigate(UiaCore.NavigateDirection.Parent), listViewItem1.AccessibilityObject); + Assert.Equal(subItemAccObj3.FragmentNavigate(UiaCore.NavigateDirection.Parent), listViewItem1.AccessibilityObject); + } + + [WinFormsFact] + public void ListViewSubItemAccessibleObject_Bounds_ReturnCorrectValue() + { + using ListView list = new ListView(); + ListViewItem listViewItem1 = new ListViewItem(new string[] { + "Test 1", + "Item 1", + "Something 1"}, -1); + + ColumnHeader columnHeader1 = new ColumnHeader(); + ColumnHeader columnHeader2 = new ColumnHeader(); + ColumnHeader columnHeader3 = new ColumnHeader(); + + list.Columns.AddRange(new ColumnHeader[] { + columnHeader1, + columnHeader2, + columnHeader3}); + list.HideSelection = false; + list.Items.Add(listViewItem1); + list.View = View.Details; + + ListViewItem.ListViewSubItem subItem = listViewItem1.SubItems[0]; + AccessibleObject accessibleObject = listViewItem1.AccessibilityObject.GetChild(0); + Assert.True(list.IsHandleCreated); + + int actualWidth = accessibleObject.Bounds.Width; + int expectedWidth = listViewItem1.SubItems[1].Bounds.X - subItem.Bounds.X; + Assert.Equal(expectedWidth, actualWidth); + + int actualHeight = accessibleObject.Bounds.Height; + int expectedHeight = subItem.Bounds.Height; + Assert.Equal(expectedHeight, actualHeight); + + Rectangle actualBounds = accessibleObject.Bounds; + actualBounds.Location = new Point(0, 0); + Rectangle expectedBounds = new Rectangle(subItem.Bounds.X, subItem.Bounds.Y, expectedWidth, expectedHeight); + expectedBounds.Location = new Point(0, 0); + Assert.Equal(expectedBounds, actualBounds); + } + + [WinFormsFact] + public void ListViewSubItemAccessibleObject_ColunmProperty_ReturnCorrectValue() + { + using ListView list = new ListView(); + ListViewItem listViewItem1 = new ListViewItem(new string[] { + "Test 1", + "Item 1", + "Something 1"}, -1); + + ColumnHeader columnHeader1 = new ColumnHeader(); + ColumnHeader columnHeader2 = new ColumnHeader(); + ColumnHeader columnHeader3 = new ColumnHeader(); + + list.Columns.AddRange(new ColumnHeader[] { + columnHeader1, + columnHeader2, + columnHeader3}); + list.HideSelection = false; + list.Items.Add(listViewItem1); + list.View = View.Details; + + AccessibleObject subItemAccObj1 = listViewItem1.AccessibilityObject.GetChild(0); + AccessibleObject subItemAccObj2 = listViewItem1.AccessibilityObject.GetChild(1); + AccessibleObject subItemAccObj3 = listViewItem1.AccessibilityObject.GetChild(2); + Assert.True(list.IsHandleCreated); + + Assert.Equal(0, subItemAccObj1.Column); + Assert.Equal(1, subItemAccObj2.Column); + Assert.Equal(2, subItemAccObj3.Column); + } + + [WinFormsFact] + public void ListViewSubItemAccessibleObject_RowProperty_ReturnCorrectValue() + { + using ListView list = new ListView(); + ListViewItem listViewItem1 = new ListViewItem(new string[] { + "Test 1", + "Item 1", + "Something 1"}, -1); + + ColumnHeader columnHeader1 = new ColumnHeader(); + ColumnHeader columnHeader2 = new ColumnHeader(); + ColumnHeader columnHeader3 = new ColumnHeader(); + + list.Columns.AddRange(new ColumnHeader[] { + columnHeader1, + columnHeader2, + columnHeader3}); + list.HideSelection = false; + list.Items.Add(listViewItem1); + list.View = View.Details; + + AccessibleObject subItemAccObj1 = listViewItem1.AccessibilityObject.GetChild(0); + AccessibleObject subItemAccObj2 = listViewItem1.AccessibilityObject.GetChild(1); + AccessibleObject subItemAccObj3 = listViewItem1.AccessibilityObject.GetChild(2); + Assert.True(list.IsHandleCreated); + + Assert.Equal(0, subItemAccObj1.Row); + Assert.Equal(0, subItemAccObj2.Row); + Assert.Equal(0, subItemAccObj3.Row); + } + } +}