diff --git a/src/System.Windows.Forms.Primitives/src/Interop/Richedit/Interop.SCF.cs b/src/System.Windows.Forms.Primitives/src/Interop/Richedit/Interop.SCF.cs index 0cc345f14bd..e26f2198b17 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/Richedit/Interop.SCF.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/Richedit/Interop.SCF.cs @@ -11,15 +11,15 @@ internal static partial class Richedit [Flags] public enum SCF : uint { - SELECTION = 0x0001, - WORD = 0x0002, - DEFAULT = 0x0000, - ALL = 0x0004, - USEUIRULES = 0x0008, - ASSOCIATEFONT = 0x0010, - NOKBUPDATE = 0x0020, - ASSOCIATEFONT2 = 0x0040, - SMARTFONT = 0x0080, + DEFAULT = 0x0000, + SELECTION = 0x0001, + WORD = 0x0002, + ALL = 0x0004, + USEUIRULES = 0x0008, + ASSOCIATEFONT = 0x0010, + NOKBUPDATE = 0x0020, + ASSOCIATEFONT2 = 0x0040, + SMARTFONT = 0x0080, CHARREPFROMLCID = 0x0100, } } diff --git a/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.ITextProvider.cs b/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.ITextProvider.cs new file mode 100644 index 00000000000..b7a5b98f7f8 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.ITextProvider.cs @@ -0,0 +1,30 @@ +// 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; + +internal static partial class Interop +{ + internal static partial class UiaCore + { + [ComImport] + [Guid("3589c92c-63f3-4367-99bb-ada653b77cf2")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ITextProvider + { + ITextRangeProvider[]? GetSelection(); + + ITextRangeProvider[]? GetVisibleRanges(); + + ITextRangeProvider? RangeFromChild(IRawElementProviderSimple childElement); + + ITextRangeProvider? RangeFromPoint(Point screenLocation); + + ITextRangeProvider? DocumentRange { get; } + + SupportedTextSelection SupportedTextSelection { get; } + } + } +} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.ITextProvider2.cs b/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.ITextProvider2.cs new file mode 100644 index 00000000000..d73dbdde0b4 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.ITextProvider2.cs @@ -0,0 +1,34 @@ +// 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; + +internal static partial class Interop +{ + internal static partial class UiaCore + { + [ComImport] + [Guid("0dc5e6ed-3e16-4bf1-8f9a-a979878bc195")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ITextProvider2 : ITextProvider + { + new ITextRangeProvider[]? GetSelection(); + + new ITextRangeProvider[]? GetVisibleRanges(); + + new ITextRangeProvider? RangeFromChild(IRawElementProviderSimple childElement); + + new ITextRangeProvider? RangeFromPoint(Point screenLocation); + + new ITextRangeProvider? DocumentRange { get; } + + new SupportedTextSelection SupportedTextSelection { get; } + + ITextRangeProvider? RangeFromAnnotation(IRawElementProviderSimple annotation); + + ITextRangeProvider? GetCaretRange(out BOOL isActive); + } + } +} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.ITextRangeProvider.cs b/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.ITextRangeProvider.cs new file mode 100644 index 00000000000..216c3519e72 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.ITextRangeProvider.cs @@ -0,0 +1,53 @@ +// 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("5347ad7b-c355-46f8-aff5-909033582f63")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ITextRangeProvider + { + ITextRangeProvider Clone(); + + BOOL Compare(ITextRangeProvider range); + + int CompareEndpoints(TextPatternRangeEndpoint endpoint, ITextRangeProvider targetRange, TextPatternRangeEndpoint targetEndpoint); + + void ExpandToEnclosingUnit(TextUnit unit); + + ITextRangeProvider? FindAttribute(int attribute, object value, BOOL backward); + + ITextRangeProvider? FindText(string text, BOOL backward, BOOL ignoreCase); + + object? GetAttributeValue(int attribute); + + double[] GetBoundingRectangles(); + + IRawElementProviderSimple GetEnclosingElement(); + + string GetText(int maxLength); + + int Move(TextUnit unit, int count); + + int MoveEndpointByUnit(TextPatternRangeEndpoint endpoint, TextUnit unit, int count); + + void MoveEndpointByRange(TextPatternRangeEndpoint endpoint, ITextRangeProvider targetRange, TextPatternRangeEndpoint targetEndpoint); + + void Select(); + + void AddToSelection(); + + void RemoveFromSelection(); + + void ScrollIntoView(BOOL alignToTop); + + IRawElementProviderSimple[] GetChildren(); + } + } +} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.SupportedTextSelection.cs b/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.SupportedTextSelection.cs new file mode 100644 index 00000000000..e9d9050a308 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.SupportedTextSelection.cs @@ -0,0 +1,19 @@ +// 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; + +internal static partial class Interop +{ + internal static partial class UiaCore + { + [Flags] + public enum SupportedTextSelection + { + None = 0, + Single = 1, + Multiple = 2 + } + } +} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.TextAttributeIdentifier.cs b/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.TextAttributeIdentifier.cs new file mode 100644 index 00000000000..96bb2d93fa6 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.TextAttributeIdentifier.cs @@ -0,0 +1,56 @@ +// 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. + +internal static partial class Interop +{ + internal static partial class UiaCore + { + public enum TextAttributeIdentifier + { + AnimationStyleAttributeId = 40000, + BackgroundColorAttributeId = 40001, + BulletStyleAttributeId = 40002, + CapStyleAttributeId = 40003, + CultureAttributeId = 40004, + FontNameAttributeId = 40005, + FontSizeAttributeId = 40006, + FontWeightAttributeId = 40007, + ForegroundColorAttributeId = 40008, + HorizontalTextAlignmentAttributeId = 40009, + IndentationFirstLineAttributeId = 40010, + IndentationLeadingAttributeId = 40011, + IndentationTrailingAttributeId = 40012, + IsHiddenAttributeId = 40013, + IsItalicAttributeId = 40014, + IsReadOnlyAttributeId = 40015, + IsSubscriptAttributeId = 40016, + IsSuperscriptAttributeId = 40017, + MarginBottomAttributeId = 40018, + MarginLeadingAttributeId = 40019, + MarginTopAttributeId = 40020, + MarginTrailingAttributeId = 40021, + OutlineStylesAttributeId = 40022, + OverlineColorAttributeId = 40023, + OverlineStyleAttributeId = 40024, + StrikethroughColorAttributeId = 40025, + StrikethroughStyleAttributeId = 40026, + TabsAttributeId = 40027, + TextFlowDirectionsAttributeId = 40028, + UnderlineColorAttributeId = 40029, + UnderlineStyleAttributeId = 40030, + AnnotationTypesAttributeId = 40031, + AnnotationObjectsAttributeId = 40032, + StyleNameAttributeId = 40033, + StyleIdAttributeId = 40034, + LinkAttributeId = 40035, + IsActiveAttributeId = 40036, + SelectionActiveEndAttributeId = 40037, + CaretPositionAttributeId = 40038, + CaretBidiModeAttributeId = 40039, + LineSpacingAttributeId = 40040, + BeforeParagraphSpacingAttributeId = 40041, + AfterParagraphSpacingAttributeId = 40042 + } + } +} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.TextPatternRangeEndpoint.cs b/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.TextPatternRangeEndpoint.cs new file mode 100644 index 00000000000..6e208f4a655 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.TextPatternRangeEndpoint.cs @@ -0,0 +1,18 @@ +// 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; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class UiaCore + { + public enum TextPatternRangeEndpoint + { + Start = 0, + End = 1 + } + } +} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.TextUnit.cs b/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.TextUnit.cs new file mode 100644 index 00000000000..a1e96a5915a --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.TextUnit.cs @@ -0,0 +1,22 @@ +// 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 + { + public enum TextUnit + { + Character = 0, + Format = 1, + Word = 2, + Line = 3, + Paragraph = 4, + Page = 5, + Document = 6 + } + } +} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.UiaCoreTypes.cs b/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.UiaCoreTypes.cs new file mode 100644 index 00000000000..f1976270dcc --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/Interop/UiaCore/Interop.UiaCoreTypes.cs @@ -0,0 +1,26 @@ +// 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 + { + private static object? s_notSupportedValue; + + [DllImport(Libraries.UiaCore, ExactSpelling = true)] + private static extern int UiaGetReservedNotSupportedValue([MarshalAs(UnmanagedType.IUnknown)] out object notSupportedValue); + + public static object UiaGetReservedNotSupportedValue() + { + if (s_notSupportedValue == null) + { + UiaGetReservedNotSupportedValue(out s_notSupportedValue); + } + + return s_notSupportedValue; + } + } +} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.Atom.cs b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.Atom.cs new file mode 100644 index 00000000000..9e6a0efe06e --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.Atom.cs @@ -0,0 +1,32 @@ +// 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; + +internal static partial class Interop +{ + /// + /// Simple wrapper for an ATOM + /// + public struct Atom + { + // #define MAXINTATOM 0xC000 + // #define MAKEINTATOM(i) (LPTSTR)((ULONG_PTR)((WORD)(i))) + // #define INVALID_ATOM ((ATOM)0) + + // Strange uses for window class atoms + // https://blogs.msdn.microsoft.com/oldnewthing/20080501-00/?p=22503/ + + public ushort ATOM; + + public Atom(ushort atom) => ATOM = atom; + + public static Atom Null = new Atom(0); + + public bool IsValid => ATOM != 0; + + public static implicit operator uint(Atom atom) => atom.ATOM; + public static implicit operator Atom(IntPtr atom) => new Atom((ushort)atom); + } +} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.ES.cs b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.ES.cs index 8a02c9daae1..cde887982d7 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.ES.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.ES.cs @@ -11,20 +11,20 @@ internal static partial class User32 [Flags] public enum ES : uint { - LEFT = 0x0000, - CENTER = 0x0001, - RIGHT = 0x0002, - MULTILINE = 0x0004, - UPPERCASE = 0x0008, - LOWERCASE = 0x0010, - PASSWORD = 0x0020, + LEFT = 0x0000, + CENTER = 0x0001, + RIGHT = 0x0002, + MULTILINE = 0x0004, + UPPERCASE = 0x0008, + LOWERCASE = 0x0010, + PASSWORD = 0x0020, AUTOVSCROLL = 0x0040, AUTOHSCROLL = 0x0080, - NOHIDESEL = 0x0100, - OEMCONVERT = 0x0400, - READONLY = 0x0800, - WANTRETURN = 0x1000, - NUMBER = 0x2000 + NOHIDESEL = 0x0100, + OEMCONVERT = 0x0400, + READONLY = 0x0800, + WANTRETURN = 0x1000, + NUMBER = 0x2000 } } } diff --git a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.GCL.cs b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.GCL.cs new file mode 100644 index 00000000000..e95525b8ccc --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.GCL.cs @@ -0,0 +1,22 @@ +// 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. + +internal static partial class Interop +{ + internal static partial class User32 + { + public enum GCL : int + { + /// + /// (GCL_WNDPROC) + /// + WNDPROC = -24, + + /// + /// (GCW_ATOM) + /// + ATOM = -32, + } + } +} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.GetCaretPos.cs b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.GetCaretPos.cs new file mode 100644 index 00000000000..a460fe3acb6 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.GetCaretPos.cs @@ -0,0 +1,15 @@ +// 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; + +internal static partial class Interop +{ + internal static partial class User32 + { + [DllImport(Libraries.User32, ExactSpelling = true)] + public static extern BOOL GetCaretPos(out Point pt); + } +} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.GetClassLong.cs b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.GetClassLong.cs new file mode 100644 index 00000000000..a9f2888269d --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.GetClassLong.cs @@ -0,0 +1,31 @@ +// 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; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class User32 + { + // We only ever call this on 32 bit so IntPtr is correct + // https://msdn.microsoft.com/library/windows/desktop/ms633580.aspx + [DllImport(Libraries.User32, SetLastError = true, ExactSpelling = true)] + private static extern IntPtr GetClassLongW(IntPtr hWnd, GCL nIndex); + + // https://msdn.microsoft.com/library/windows/desktop/ms633581.aspx + [DllImport(Libraries.User32, SetLastError = true, ExactSpelling = true)] + private static extern IntPtr GetClassLongPtrW(IntPtr hWnd, GCL nIndex); + + public static IntPtr GetClassLong(IntPtr hWnd, GCL nIndex) + { + if (IntPtr.Size == 4) + { + return GetClassLongW(hWnd, nIndex); + } + + return GetClassLongPtrW(hWnd, nIndex); + } + } +} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.RegisterClassW.cs b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.RegisterClassW.cs index d6b4efda8b7..9192ff50b06 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.RegisterClassW.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.RegisterClassW.cs @@ -2,7 +2,6 @@ // 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; using System.Runtime.InteropServices; internal static partial class Interop @@ -10,6 +9,6 @@ internal static partial class Interop internal static partial class User32 { [DllImport(Libraries.User32, CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)] - public static extern ushort RegisterClassW(ref WNDCLASS lpWndClass); + public static extern Atom RegisterClassW(ref WNDCLASS lpWndClass); } } diff --git a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.SendMessageW.cs b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.SendMessageW.cs index 99dc582bc92..a1e50d76eb2 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.SendMessageW.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.SendMessageW.cs @@ -97,5 +97,19 @@ public unsafe static IntPtr SendMessageW( return SendMessageW(hWnd, Msg, wParam, (IntPtr)l); } } + + public unsafe static IntPtr SendMessageW( + IHandle hWnd, + WM Msg, + ref TWParam wParam, + ref TLParam lParam) + where TWParam : unmanaged + where TLParam : unmanaged + { + fixed (void* w = &wParam, l = &lParam) + { + return SendMessageW(hWnd, Msg, (IntPtr)w, (IntPtr)l); + } + } } -} \ No newline at end of file +} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.SetClassLong.cs b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.SetClassLong.cs index 0761eee2d25..4d26404bba9 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.SetClassLong.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.SetClassLong.cs @@ -4,7 +4,6 @@ using System; using System.Runtime.InteropServices; -using System.Windows.Forms; internal static partial class Interop { @@ -23,12 +22,8 @@ public static IntPtr SetClassLong(IntPtr hWnd, GCL nIndex, IntPtr dwNewLong) { return SetClassLongW(hWnd, nIndex, dwNewLong); } - return SetClassLongPtrW(hWnd, nIndex, dwNewLong); - } - public enum GCL : int - { - WNDPROC = -24 + return SetClassLongPtrW(hWnd, nIndex, dwNewLong); } } } diff --git a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.VK.cs b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.VK.cs index 33022d4d46b..34c82477a8c 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.VK.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.VK.cs @@ -27,6 +27,11 @@ public static class VK public const int SCROLL = 0x0091; public const int INSERT = 0x002D; public const int DELETE = 0x002E; + public const int LWIN = 0x5B; + public const int RWIN = 0x5C; + public const int APPS = 0x5D; + public const int RCONTROL = 0xA3; + public const int RMENU = 0xA5; } } } diff --git a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.WS.cs b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.WS.cs index b88adcbbed5..41e71ce8943 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.WS.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.WS.cs @@ -14,25 +14,25 @@ internal static partial class User32 [Flags] public enum WS : uint { - OVERLAPPED = 0x00000000, - POPUP = 0x80000000, - CHILD = 0x40000000, - MINIMIZE = 0x20000000, - VISIBLE = 0x10000000, - DISABLED = 0x08000000, - CLIPSIBLINGS = 0x04000000, - CLIPCHILDREN = 0x02000000, - MAXIMIZE = 0x01000000, - CAPTION = 0x00C00000, - BORDER = 0x00800000, - DLGFRAME = 0x00400000, - VSCROLL = 0x00200000, - HSCROLL = 0x00100000, - SYSMENU = 0x00080000, - THICKFRAME = 0x00040000, - TABSTOP = 0x00010000, - MINIMIZEBOX = 0x00020000, - MAXIMIZEBOX = 0x00010000 + OVERLAPPED = 0x00000000, + POPUP = 0x80000000, + CHILD = 0x40000000, + MINIMIZE = 0x20000000, + VISIBLE = 0x10000000, + DISABLED = 0x08000000, + CLIPSIBLINGS = 0x04000000, + CLIPCHILDREN = 0x02000000, + MAXIMIZE = 0x01000000, + CAPTION = 0x00C00000, + BORDER = 0x00800000, + DLGFRAME = 0x00400000, + VSCROLL = 0x00200000, + HSCROLL = 0x00100000, + SYSMENU = 0x00080000, + THICKFRAME = 0x00040000, + TABSTOP = 0x00010000, + MINIMIZEBOX = 0x00020000, + MAXIMIZEBOX = 0x00010000 } } } diff --git a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.WS_EX.cs b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.WS_EX.cs index 70c3b02987f..81a810cb577 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.WS_EX.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.WS_EX.cs @@ -14,33 +14,34 @@ internal static partial class User32 [Flags] public enum WS_EX : uint { - DLGMODALFRAME = 0x00000001, - NOPARENTNOTIFY = 0x00000004, - TOPMOST = 0x00000008, - ACCEPTFILES = 0x00000010, - TRANSPARENT = 0x00000020, - MDICHILD = 0x00000040, - TOOLWINDOW = 0x00000080, - WINDOWEDGE = 0x00000100, - CLIENTEDGE = 0x00000200, - CONTEXTHELP = 0x00000400, - RIGHT = 0x00001000, - LEFT = 0x00000000, - RTLREADING = 0x00002000, - LTRREADING = 0x00000000, - LEFTSCROLLBAR = 0x00004000, - RIGHTSCROLLBAR = 0x00000000, - CONTROLPARENT = 0x00010000, - STATICEDGE = 0x00020000, - APPWINDOW = 0x00040000, - OVERLAPPEDWINDOW = WINDOWEDGE | CLIENTEDGE, - PALETTEWINDOW = WINDOWEDGE | TOOLWINDOW | TOPMOST, - LAYERED = 0x00080000, - NOINHERITLAYOUT = 0x00100000, + DEFAULT = 0x00000000, + DLGMODALFRAME = 0x00000001, + NOPARENTNOTIFY = 0x00000004, + TOPMOST = 0x00000008, + ACCEPTFILES = 0x00000010, + TRANSPARENT = 0x00000020, + MDICHILD = 0x00000040, + TOOLWINDOW = 0x00000080, + WINDOWEDGE = 0x00000100, + CLIENTEDGE = 0x00000200, + CONTEXTHELP = 0x00000400, + RIGHT = 0x00001000, + LEFT = 0x00000000, + RTLREADING = 0x00002000, + LTRREADING = 0x00000000, + LEFTSCROLLBAR = 0x00004000, + RIGHTSCROLLBAR = 0x00000000, + CONTROLPARENT = 0x00010000, + STATICEDGE = 0x00020000, + APPWINDOW = 0x00040000, + OVERLAPPEDWINDOW = WINDOWEDGE | CLIENTEDGE, + PALETTEWINDOW = WINDOWEDGE | TOOLWINDOW | TOPMOST, + LAYERED = 0x00080000, + NOINHERITLAYOUT = 0x00100000, NOREDIRECTIONBITMAP = 0x00200000, - LAYOUTRTL = 0x00400000, - COMPOSITED = 0x02000000, - NOACTIVATE = 0x08000000 + LAYOUTRTL = 0x00400000, + COMPOSITED = 0x02000000, + NOACTIVATE = 0x08000000 } } } diff --git a/src/System.Windows.Forms.Primitives/src/PublicAPI.Unshipped.txt b/src/System.Windows.Forms.Primitives/src/PublicAPI.Unshipped.txt index e69de29bb2d..0122d2c3883 100644 --- a/src/System.Windows.Forms.Primitives/src/PublicAPI.Unshipped.txt +++ b/src/System.Windows.Forms.Primitives/src/PublicAPI.Unshipped.txt @@ -0,0 +1,3 @@ +System.Windows.Forms.Automation.IAutomationLiveRegion +System.Windows.Forms.Automation.IAutomationLiveRegion.LiveSetting.get -> System.Windows.Forms.Automation.AutomationLiveSetting +System.Windows.Forms.Automation.IAutomationLiveRegion.LiveSetting.set -> void \ No newline at end of file diff --git a/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/CapStyle.cs b/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/CapStyle.cs new file mode 100644 index 00000000000..0f233c2a01c --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/CapStyle.cs @@ -0,0 +1,18 @@ +// 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. + +namespace System.Windows.Forms.Automation +{ + internal enum CapStyle + { + Other = -1, + None = 0, + SmallCap = 1, + AllCap = 2, + AllPetiteCaps = 3, + PetiteCaps = 4, + Unicase = 5, + Titling = 6 + } +} diff --git a/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/HorizontalTextAlignment.cs b/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/HorizontalTextAlignment.cs new file mode 100644 index 00000000000..9cf5b8c4b46 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/HorizontalTextAlignment.cs @@ -0,0 +1,14 @@ +// 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. + +namespace System.Windows.Forms.Automation +{ + internal enum HorizontalTextAlignment + { + Left = 0, + Centered = 1, + Right = 2, + Justified = 3 + } +} diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/Automation/IAutomationLiveRegion.cs b/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/IAutomationLiveRegion.cs similarity index 100% rename from src/System.Windows.Forms/src/System/Windows/Forms/Automation/IAutomationLiveRegion.cs rename to src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/IAutomationLiveRegion.cs diff --git a/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/TextDecorationLineStyle.cs b/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/TextDecorationLineStyle.cs new file mode 100644 index 00000000000..23f94c03b55 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/TextDecorationLineStyle.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. + +namespace System.Windows.Forms.Automation +{ + internal enum TextDecorationLineStyle + { + Other = -1, + None = 0, + Single = 1, + WordsOnly = 2, + Double = 3, + Dot = 4, + Dash = 5, + DashDot = 6, + DashDotDot = 7, + Wavy = 8, + ThickSingle = 9, + DoubleWavy = 11, + ThickWavy = 12, + LongDash = 13, + ThickDash = 14, + ThickDashDot = 15, + ThickDashDotDot = 16, + ThickDot = 17, + ThickLongDash = 18 + } +} diff --git a/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/UiaTextProvider.cs b/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/UiaTextProvider.cs new file mode 100644 index 00000000000..8e8bb6f224d --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/UiaTextProvider.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.Collections.Generic; +using System.Drawing; +using System.Runtime.InteropServices; +using static Interop.Gdi32; +using static Interop.UiaCore; +using static Interop.User32; + +namespace System.Windows.Forms.Automation +{ + internal abstract class UiaTextProvider : ITextProvider + { + /// + /// The value of a width of an end of a text line as 2 px to a ScreenReader can show it. + /// + public const int EndOfLineWidth = 2; + + public abstract ITextRangeProvider[]? GetSelection(); + + public abstract ITextRangeProvider[]? GetVisibleRanges(); + + public abstract ITextRangeProvider? RangeFromChild(IRawElementProviderSimple childElement); + + public abstract ITextRangeProvider? RangeFromPoint(Point screenLocation); + + public abstract ITextRangeProvider? DocumentRange { get; } + + public abstract SupportedTextSelection SupportedTextSelection { get; } + + public abstract Rectangle BoundingRectangle { get; } + + public abstract ES EditStyle { get; } + + public abstract int FirstVisibleLine { get; } + + public abstract bool IsMultiline { get; } + + public abstract bool IsReadingRTL { get; } + + public abstract bool IsReadOnly { get; } + + public abstract bool IsScrollable { get; } + + public abstract int LinesPerPage { get; } + + public abstract int LinesCount { get; } + + public abstract LOGFONTW Logfont { get; } + + public abstract string Text { get; } + + public abstract int TextLength { get; } + + public abstract WS_EX WindowExStyle { get; } + + public abstract WS WindowStyle { get; } + + public abstract int GetLineFromCharIndex(int charIndex); + + public abstract int GetLineIndex(int line); + + public abstract Point GetPositionFromChar(int charIndex); + + public abstract Point GetPositionFromCharForUpperRightCorner(int startCharIndex, string text); + + public abstract void GetVisibleRangePoints(out int visibleStart, out int visibleEnd); + + public abstract bool LineScroll(int charactersHorizontal, int linesVertical); + + public abstract Point PointToScreen(Point pt); + + public abstract void SetSelection(int start, int end); + + public ES GetEditStyle(IntPtr hWnd) => hWnd != IntPtr.Zero ? (ES)GetWindowLong(new HandleRef(null, hWnd), GWL.STYLE) : ES.LEFT; + + public WS_EX GetWindowExStyle(IntPtr hWnd) => hWnd != IntPtr.Zero ? (WS_EX)GetWindowLong(new HandleRef(null, hWnd), GWL.EXSTYLE) : WS_EX.LEFT; + + public WS GetWindowStyle(IntPtr hWnd) => hWnd != IntPtr.Zero ? (WS)GetWindowLong(new HandleRef(this, hWnd), GWL.STYLE) : WS.DISABLED; + + public double[] RectListToDoubleArray(List rectArray) + { + if (rectArray == null || rectArray.Count == 0) + { + return Array.Empty(); + } + + double[] doubles = new double[rectArray.Count * 4]; + int scan = 0; + + for (int i = 0; i < rectArray.Count; i++) + { + doubles[scan++] = rectArray[i].X; + doubles[scan++] = rectArray[i].Y; + doubles[scan++] = rectArray[i].Width; + doubles[scan++] = rectArray[i].Height; + } + + return doubles; + } + + public int SendInput(int inputs, ref INPUT input, int size) + { + Span currentInput = stackalloc INPUT[1]; + currentInput[0] = input; + + return (int)Interop.User32.SendInput((uint)inputs, currentInput, size); + } + + public unsafe int SendKeyboardInputVK(short vk, bool press) + { + INPUT keyboardInput = new INPUT(); + + keyboardInput.type = INPUTENUM.KEYBOARD; + keyboardInput.inputUnion.ki.wVk = (ushort)vk; + keyboardInput.inputUnion.ki.wScan = 0; + keyboardInput.inputUnion.ki.dwFlags = press ? 0 : KEYEVENTF.KEYUP; + + if (IsExtendedKey(vk)) + { + keyboardInput.inputUnion.ki.dwFlags |= KEYEVENTF.EXTENDEDKEY; + } + + keyboardInput.inputUnion.ki.time = 0; + keyboardInput.inputUnion.ki.dwExtraInfo = IntPtr.Zero; + + return SendInput(1, ref keyboardInput, sizeof(INPUT)); + } + + private static bool IsExtendedKey(short vk) + { + // From the SDK: + // The extended-key flag indicates whether the keystroke message originated from one of + // the additional keys on the enhanced keyboard. The extended keys consist of the ALT and + // CTRL keys on the right-hand side of the keyboard; the INS, DEL, HOME, END, PAGE UP, + // PAGE DOWN, and arrow keys in the clusters to the left of the numeric keypad; the NUM LOCK + // key; the BREAK (CTRL+PAUSE) key; the PRINT SCRN key; and the divide (/) and ENTER keys in + // the numeric keypad. The extended-key flag is set if the key is an extended key. + // + // - docs appear to be incorrect. Use of Spy++ indicates that break is not an extended key. + // Also, menu key and windows keys also appear to be extended. + return vk == unchecked((short)VK.RMENU) || + vk == unchecked((short)VK.RCONTROL) || + vk == unchecked((short)VK.NUMLOCK) || + vk == unchecked((short)VK.INSERT) || + vk == unchecked((short)VK.DELETE) || + vk == unchecked((short)VK.HOME) || + vk == unchecked((short)VK.END) || + vk == unchecked((short)VK.PRIOR) || + vk == unchecked((short)VK.NEXT) || + vk == unchecked((short)VK.UP) || + vk == unchecked((short)VK.DOWN) || + vk == unchecked((short)VK.LEFT) || + vk == unchecked((short)VK.RIGHT) || + vk == unchecked((short)VK.APPS) || + vk == unchecked((short)VK.RWIN) || + vk == unchecked((short)VK.LWIN); + // Note that there are no distinct values for the following keys: + // numpad divide + // numpad enter + } + } +} diff --git a/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/UiaTextProvider2.cs b/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/UiaTextProvider2.cs new file mode 100644 index 00000000000..7deb6b8a26c --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/UiaTextProvider2.cs @@ -0,0 +1,15 @@ +// 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; + +namespace System.Windows.Forms.Automation +{ + internal abstract class UiaTextProvider2 : UiaTextProvider, UiaCore.ITextProvider2 + { + public abstract UiaCore.ITextRangeProvider? GetCaretRange(out BOOL isActive); + + public abstract UiaCore.ITextRangeProvider? RangeFromAnnotation(UiaCore.IRawElementProviderSimple annotationElement); + } +} diff --git a/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/UiaTextRange.cs b/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/UiaTextRange.cs new file mode 100644 index 00000000000..478cf0cda5c --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Automation/UiaTextRange.cs @@ -0,0 +1,958 @@ +// 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.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using static Interop; +using static Interop.UiaCore; +using static Interop.User32; + +namespace System.Windows.Forms.Automation +{ + internal class UiaTextRange : ITextRangeProvider + { + // Edit controls always use "\r\n" as the line separator, not "\n". + // This string is a non-localizable string. + private const string LineSeparator = "\r\n"; + + private readonly IRawElementProviderSimple _enclosingElement; + private readonly UiaTextProvider _provider; + + private int _start; + private int _end; + + /// + /// A caret position before the first character from a text range, not an index of an item. + /// + /// + /// A caret position after the last character from a text range, not an index of an item. + /// + /// + /// If there is a range "Test string", then start = 0, end = 11. + /// If start = 2 and end = 9, the range is "et stri". + /// If start=end, that points a caret position only, there is no any text range. + /// + public UiaTextRange(IRawElementProviderSimple enclosingElement, UiaTextProvider provider, int start, int end) + { + _enclosingElement = enclosingElement ?? throw new ArgumentNullException(nameof(enclosingElement)); + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + + if (start > 0) + { + _start = start; + _end = start; + } + + if (end > _start) + { + _end = end; + } + } + + /// + /// Last caret position of this text range. + /// + internal int End + { + get => _end; + set + { + // Ensure that we never accidentally get a negative index. + if (value < 0) + { + _end = 0; + } + else + { + _end = value; + } + + // Ensure that end never moves before start. + if (_end < _start) + { + _start = _end; + } + } + } + + internal int Length + { + get + { + if (Start < 0 || End < 0 || Start > End) + { + return 0; + } + + // The subtraction of caret positions returns the length of the text. + return End - Start; + } + } + + /// + /// First caret position of this text range. + /// + internal int Start + { + get => _start; + set + { + // Ensure that start never moves after end. + if (value > _end) + { + _end = value; + } + + // Ensure that we never accidentally get a negative index. + if (value < 0) + { + _start = 0; + } + else + { + _start = value; + } + } + } + + /// + /// Strictly only needs to be == since never should _start > _end. + /// + private bool IsDegenerate => _start == _end; + + ITextRangeProvider ITextRangeProvider.Clone() => new UiaTextRange(_enclosingElement, _provider, Start, End); + + /// + /// Ranges come from the same element. Only need to compare endpoints. + /// + BOOL ITextRangeProvider.Compare(ITextRangeProvider range) + => (range is UiaTextRange editRange && editRange.Start == Start && editRange.End == End).ToBOOL(); + + int ITextRangeProvider.CompareEndpoints( + TextPatternRangeEndpoint endpoint, + ITextRangeProvider targetRange, + TextPatternRangeEndpoint targetEndpoint) + { + if (!(targetRange is UiaTextRange editRange)) + { + return -1; + } + + int e1 = (endpoint == (int)TextPatternRangeEndpoint.Start) ? Start : End; + int e2 = (targetEndpoint == (int)TextPatternRangeEndpoint.Start) ? editRange.Start : editRange.End; + + return e1 - e2; + } + + void ITextRangeProvider.ExpandToEnclosingUnit(TextUnit unit) + { + switch (unit) + { + case TextUnit.Character: + // Leave it as it is except the case with 0-range. + if (IsDegenerate) + { + End = MoveEndpointForward(End, TextUnit.Character, 1, out int moved); + } + break; + + case TextUnit.Word: + { + // Get the word boundaries. + string text = _provider.Text; + ValidateEndpoints(); + + // Move start left until we reach a word boundary. + while (!AtWordBoundary(text, Start)) + { + Start--; + } + + // Move end right until we reach word boundary (different from Start). + End = Math.Min(Math.Max(End, Start + 1), text.Length); + + while (!AtWordBoundary(text, End)) + { + End++; + } + } + break; + + case TextUnit.Line: + { + if (_provider.LinesCount != 1) + { + int startLine = _provider.GetLineFromCharIndex(Start); + int startIndex = _provider.GetLineIndex(startLine); + + int endLine = _provider.GetLineFromCharIndex(End); + int endIndex; + if (endLine < _provider.LinesCount - 1) + { + endLine++; + endIndex = _provider.GetLineIndex(endLine); + } + else + { + endIndex = _provider.TextLength; + } + + MoveTo(startIndex, endIndex); + } + else + { + MoveTo(0, _provider.TextLength); + } + } + break; + + case TextUnit.Paragraph: + { + // Get the paragraph boundaries. + string text = _provider.Text; + ValidateEndpoints(); + + // Move start left until we reach a paragraph boundary. + while (!AtParagraphBoundary(text, Start)) + { + Start--; + } + + // Move end right until we reach a paragraph boundary (different from Start). + End = Math.Min(Math.Max(End, Start + 1), text.Length); + + while (!AtParagraphBoundary(text, End)) + { + End++; + } + } + break; + + case TextUnit.Format: + case TextUnit.Page: + case TextUnit.Document: + MoveTo(0, _provider.TextLength); + break; + + default: + throw new InvalidEnumArgumentException(nameof(unit), (int)unit, typeof(TextUnit)); + } + } + + ITextRangeProvider? ITextRangeProvider.FindAttribute(int attributeId, object val, BOOL backwards) => null; + + ITextRangeProvider? ITextRangeProvider.FindText(string text, BOOL backwards, BOOL ignoreCase) + { + if (text is null) + { + Debug.Fail("Invalid text range argument. 'text' should not be null."); + return null; + } + + if (text.Length == 0) + { + Debug.Fail("Invalid text range argument. 'text' length should be more than 0."); + return null; + } + + ValidateEndpoints(); + ReadOnlySpan rangeText = new ReadOnlySpan(_provider.Text.ToCharArray(), Start, Length); + StringComparison comparisonType = ignoreCase.IsTrue() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + + // Do a case-sensitive search for the text inside the range. + int index = backwards.IsTrue() ? rangeText.LastIndexOf(text, comparisonType) : rangeText.IndexOf(text, comparisonType); + + // If the text was found then create a new range covering the found text. + return index >= 0 ? new UiaTextRange(_enclosingElement, _provider, Start + index, Start + index + text.Length) : null; + } + + object? ITextRangeProvider.GetAttributeValue(int attributeId) => GetAttributeValue((TextAttributeIdentifier)attributeId); + + double[] ITextRangeProvider.GetBoundingRectangles() + { + // if this is an end of line + if (Start == _provider.TextLength) + { + Point endlinePoint; + User32.GetCaretPos(out endlinePoint); + endlinePoint = _provider.PointToScreen(endlinePoint); + Rectangle endlineRectangle = new Rectangle(endlinePoint.X, endlinePoint.Y + 2, UiaTextProvider.EndOfLineWidth, Math.Abs(_provider.Logfont.lfHeight) + 1); + return new double[] { endlineRectangle.X, endlineRectangle.Y, endlineRectangle.Width, endlineRectangle.Height }; + } + + // Return zero rectangles for a degenerate-range. We don't return an empty, + // but properly positioned, rectangle for degenerate ranges. + if (IsDegenerate) + { + return Array.Empty(); + } + + string text = _provider.Text; + ValidateEndpoints(); + Rectangle ownerBounds = Drawing.Rectangle.Empty; + + if (_enclosingElement.GetPropertyValue(UIA.BoundingRectanglePropertyId) is object boundsPropertyValue) + { + ownerBounds = (Rectangle)boundsPropertyValue; + } + + // Get the mapping from client coordinates to screen coordinates. + Point mapClientToScreen = new Point(ownerBounds.X, ownerBounds.Y); + + // Clip the rectangles to the edit control's formatting rectangle. + Rectangle clippingRectangle = _provider.BoundingRectangle; + + // We accumulate rectangles onto a list. + List rectangles; + + if (_provider.IsMultiline) + { + rectangles = GetMultilineBoundingRectangles(text, mapClientToScreen, clippingRectangle); + return _provider.RectListToDoubleArray(rectangles); + } + + rectangles = new List(); + + // Figure out the rectangle for this one line. + Point startPoint = _provider.GetPositionFromChar(Start); + Point endPoint = _provider.GetPositionFromCharForUpperRightCorner(End - 1, text); + + // Add 2 to Y to get a correct size of a rectangle around a range + Rectangle rectangle = new Rectangle(startPoint.X, startPoint.Y + 2, endPoint.X - startPoint.X, clippingRectangle.Height); + rectangle.Intersect(clippingRectangle); + + if (rectangle.Width > 0 && rectangle.Height > 0) + { + rectangle.Offset(mapClientToScreen.X, mapClientToScreen.Y); + rectangles.Add(rectangle); + } + + return _provider.RectListToDoubleArray(rectangles); + } + + IRawElementProviderSimple ITextRangeProvider.GetEnclosingElement() => _enclosingElement; + + string ITextRangeProvider.GetText(int maxLength) + { + if (maxLength == -1) + { + maxLength = End + 1; + } + + string text = _provider.Text; + ValidateEndpoints(); + maxLength = maxLength >= 0 ? Math.Min(Length, maxLength) : Length; + + return text.Length < maxLength - Start + ? text.Substring(Start) + : text.Substring(Start, maxLength); + } + + int ITextRangeProvider.Move(TextUnit unit, int count) + { + // Positive count means move forward. Negative count means move backwards. + int moved; + + if (count > 0) + { + // If the range is non-degenerate then we need to collapse the range. + // (See the discussion of Count for ITextRange::Move) + if (!IsDegenerate) + { + // If the count is greater than zero, collapse the range at its end point. + Start = End; + } + + // Move the degenerate range forward by the number of units. + int start = Start; + Start = MoveEndpointForward(Start, unit, count, out moved); + + // If the start did not change then no move was done. + if (start != Start) + { + return moved; + } + } + + if (count < 0) + { + // If the range is non-degenerate then we need to collapse the range. + if (!IsDegenerate) + { + // If the count is less than zero, collapse the range at the starting point. + End = Start; + } + + // Move the degenerate range backward by the number of units. + int end = End; + End = MoveEndpointBackward(End, unit, count, out moved); + + // If the end did not change then no move was done. + if (end != End) + { + return moved; + } + } + + // Moving zero of any unit has no effect. + return 0; + } + + int ITextRangeProvider.MoveEndpointByUnit( + TextPatternRangeEndpoint endpoint, + TextUnit unit, int count) + { + // Positive count means move forward. Negative count means move backwards. + bool moveStart = endpoint == TextPatternRangeEndpoint.Start; + int moved; + int start = Start; + int end = End; + + if (count > 0) + { + if (moveStart) + { + Start = MoveEndpointForward(Start, unit, count, out moved); + + // If the start did not change then no move was done. + return start == Start ? 0 : moved; + } + + End = MoveEndpointForward(End, unit, count, out moved); + + // If the end did not change then no move was done. + return end == End ? 0 : moved; + } + + if (count < 0) + { + if (moveStart) + { + Start = MoveEndpointBackward(Start, unit, count, out moved); + + // If the start did not change then no move was done. + return start == Start ? 0 : moved; + } + + End = MoveEndpointBackward(End, unit, count, out moved); + + // If the end did not change then no move was done. + return end == End ? 0 : moved; + } + + // Moving zero of any unit has no effect. + return 0; + } + + void ITextRangeProvider.MoveEndpointByRange( + TextPatternRangeEndpoint endpoint, + ITextRangeProvider targetRange, + TextPatternRangeEndpoint targetEndpoint) + { + if (!(targetRange is UiaTextRange textRange)) + { + return; + } + + int e = (targetEndpoint == TextPatternRangeEndpoint.Start) + ? textRange.Start + : textRange.End; + + if (endpoint == TextPatternRangeEndpoint.Start) + { + Start = e; + } + else + { + End = e; + } + } + + void ITextRangeProvider.Select() => _provider.SetSelection(Start, End); + + /// + /// Do nothing. Do not throw exception. + /// + void ITextRangeProvider.AddToSelection() + { } + + /// + /// Do nothing. Do not throw exception. + /// + void ITextRangeProvider.RemoveFromSelection() + { } + + void ITextRangeProvider.ScrollIntoView(BOOL alignToTop) + { + if (_provider.IsMultiline) + { + int newFirstLine = alignToTop.IsTrue() + ? _provider.GetLineFromCharIndex(Start) + : Math.Max(0, _provider.GetLineFromCharIndex(End) - _provider.LinesPerPage + 1); + + _provider.LineScroll(Start, newFirstLine - _provider.FirstVisibleLine); + + return; + } + + if (_provider.IsScrollable) + { + _provider.GetVisibleRangePoints(out int visibleStart, out int visibleEnd); + short key = (short)(Start > visibleStart ? VK.RIGHT : VK.LEFT); + + if (_provider.IsReadingRTL) + { + if (Start > visibleStart || Start < visibleEnd) + { + _provider.SendKeyboardInputVK(key, true); + _provider.GetVisibleRangePoints(out visibleStart, out visibleEnd); + } + + return; + } + + if (Start < visibleStart || Start > visibleEnd) + { + _provider.SendKeyboardInputVK(key, true); + _provider.GetVisibleRangePoints(out visibleStart, out visibleEnd); + } + } + } + + /// + /// We don't have any children so return an empty array + /// + IRawElementProviderSimple[] ITextRangeProvider.GetChildren() => Array.Empty(); + + /// + /// Returns true if index identifies a paragraph boundary within text. + /// + private static bool AtParagraphBoundary(string text, int index) + => string.IsNullOrWhiteSpace(text) || index <= 0 || index >= text.Length || ((text[index - 1] == '\n') && (text[index] != '\n')); + + private static bool AtWordBoundary(string text, int index) + { + // Returns true if index identifies a word boundary within text. + // Following richedit & word precedent the boundaries are at the leading edge of the word + // so the span of a word includes trailing whitespace. + + // We are at a word boundary if we are at the beginning or end of the text. + if (string.IsNullOrWhiteSpace(text) || index <= 0 || index >= text.Length || AtParagraphBoundary(text, index)) + { + return true; + } + + char ch1 = text[index - 1]; + char ch2 = text[index]; + + // An apostrophe does *not* break a word if it follows or precedes characters. + if ((char.IsLetterOrDigit(ch1) && IsApostrophe(ch2)) + || (IsApostrophe(ch1) && char.IsLetterOrDigit(ch2) && index >= 2 && char.IsLetterOrDigit(text[index - 2]))) + { + return false; + } + + // The following transitions mark boundaries. + // Note: these are constructed to include trailing whitespace. + return (char.IsWhiteSpace(ch1) && !char.IsWhiteSpace(ch2)) + || (char.IsLetterOrDigit(ch1) && !char.IsLetterOrDigit(ch2)) + || (!char.IsLetterOrDigit(ch1) && char.IsLetterOrDigit(ch2)) + || (char.IsPunctuation(ch1) && char.IsWhiteSpace(ch2)); + } + + private static bool IsApostrophe(char ch) => ch == '\'' || ch == (char)0x2019; // Unicode Right Single Quote Mark + + private object? GetAttributeValue(TextAttributeIdentifier textAttributeIdentifier) + { + return textAttributeIdentifier switch + { + TextAttributeIdentifier.BackgroundColorAttributeId => GetBackgroundColor(), + TextAttributeIdentifier.CapStyleAttributeId => GetCapStyle(_provider.EditStyle), + TextAttributeIdentifier.FontNameAttributeId => GetFontName(_provider.Logfont), + TextAttributeIdentifier.FontSizeAttributeId => GetFontSize(_provider.Logfont), + TextAttributeIdentifier.FontWeightAttributeId => GetFontWeight(_provider.Logfont), + TextAttributeIdentifier.ForegroundColorAttributeId => GetForegroundColor(), + TextAttributeIdentifier.HorizontalTextAlignmentAttributeId => GetHorizontalTextAlignment(_provider.EditStyle), + TextAttributeIdentifier.IsItalicAttributeId => GetItalic(_provider.Logfont), + TextAttributeIdentifier.IsReadOnlyAttributeId => GetReadOnly(), + TextAttributeIdentifier.StrikethroughStyleAttributeId => GetStrikethroughStyle(_provider.Logfont), + TextAttributeIdentifier.UnderlineStyleAttributeId => GetUnderlineStyle(_provider.Logfont), + _ => UiaGetReservedNotSupportedValue() + }; + } + + /// + /// Helper function to accumulate a list of bounding rectangles for a potentially mult-line range. + /// + private List GetMultilineBoundingRectangles(string text, Point mapClientToScreen, Rectangle clippingRectangle) + { + // Remember the line height. + int height = Math.Abs(_provider.Logfont.lfHeight); + + // Get the starting and ending lines for the range. + int start = Start; + int end = End; + + int startLine = _provider.GetLineFromCharIndex(start); + int endLine = _provider.GetLineFromCharIndex(end - 1); + + // Adjust the start based on the first visible line. + int firstVisibleLine = _provider.FirstVisibleLine; + if (firstVisibleLine > startLine) + { + startLine = firstVisibleLine; + start = _provider.GetLineIndex(startLine); + } + + // Adjust the end based on the last visible line. + int lastVisibleLine = firstVisibleLine + _provider.LinesPerPage - 1; + if (lastVisibleLine < endLine) + { + endLine = lastVisibleLine; + end = _provider.GetLineIndex(endLine) - 1; + } + + // Adding a rectangle for each line. + List rects = new List(); + int nextLineIndex = _provider.GetLineIndex(startLine); + + for (int i = startLine; i <= endLine; i++) + { + // Determine the starting coordinate on this line. + Point startPoint = _provider.GetPositionFromChar(i == startLine ? start : nextLineIndex); + + // Determine the ending coordinate on this line. + Point endPoint; + + if (i == endLine) + { + endPoint = _provider.GetPositionFromCharForUpperRightCorner(end - 1, text); + } + else + { + nextLineIndex = _provider.GetLineIndex(i + 1); + endPoint = _provider.GetPositionFromChar(nextLineIndex - 1); + } + + // Add a bounding rectangle for this line if it is nonempty. + // Add 2 to Y and 1 to Height to get a correct size of a rectangle around a range + Rectangle rect = new Rectangle(startPoint.X, startPoint.Y + 2, endPoint.X - startPoint.X, height + 1); + rect.Intersect(clippingRectangle); + if (rect.Width > 0 && rect.Height > 0) + { + rect.Offset(mapClientToScreen.X, mapClientToScreen.Y); + rects.Add(rect); + } + } + + return rects; + } + + private HorizontalTextAlignment GetHorizontalTextAlignment(ES editStyle) + { + if (editStyle.HasFlag(ES.CENTER)) + { + return HorizontalTextAlignment.Centered; + } + + if (editStyle.HasFlag(ES.RIGHT)) + { + return HorizontalTextAlignment.Right; + } + + return HorizontalTextAlignment.Left; + } + + private CapStyle GetCapStyle(ES editStyle) => editStyle.HasFlag(ES.UPPERCASE) ? CapStyle.AllCap : CapStyle.None; + + private bool GetReadOnly() => _provider.IsReadOnly; + + private static COLORREF GetBackgroundColor() => GetSysColor(COLOR.WINDOW); + + private static string GetFontName(LOGFONTW logfont) => logfont.FaceName.ToString(); + + private double GetFontSize(LOGFONTW logfont) + { + // Note: this assumes integral point sizes. violating this assumption would confuse the user + // because they set something to 7 point but reports that it is, say 7.2 point, due to the rounding. + using var dc = User32.GetDcScope.ScreenDC; + int lpy = Gdi32.GetDeviceCaps(dc, Gdi32.DeviceCapability.LOGPIXELSY); + return Math.Round((double)(-logfont.lfHeight) * 72 / lpy); + } + + private static Gdi32.FW GetFontWeight(LOGFONTW logfont) => logfont.lfWeight; + + private static COLORREF GetForegroundColor() => GetSysColor(COLOR.WINDOWTEXT); + + private static bool GetItalic(LOGFONTW logfont) => logfont.lfItalic != 0; + + private static TextDecorationLineStyle GetStrikethroughStyle(LOGFONTW logfont) + => logfont.lfStrikeOut != 0 ? TextDecorationLineStyle.Single : TextDecorationLineStyle.None; + + private static TextDecorationLineStyle GetUnderlineStyle(LOGFONTW logfont) + => logfont.lfUnderline != 0 ? TextDecorationLineStyle.Single : TextDecorationLineStyle.None; + + /// + /// Moves an endpoint forward a certain number of units. + /// + private int MoveEndpointForward(int index, TextUnit unit, int count, out int moved) + { + switch (unit) + { + case TextUnit.Character: + { + int limit = _provider.TextLength; + ValidateEndpoints(); + + moved = Math.Min(count, limit - index); + index = index + moved; + + index = index > limit ? limit : index; + } + break; + + case TextUnit.Word: + { + string text = _provider.Text; + ValidateEndpoints(); + moved = 0; + + while (moved < count && index < text.Length) + { + index++; + + while (!AtWordBoundary(text, index)) + { + index++; + } + + moved++; + } + } + break; + + case TextUnit.Line: + { + // Figure out what line we are on. If we are in the middle of a line and + // are moving left then we'll round up to the next line so that we move + // to the beginning of the current line. + int line = _provider.GetLineFromCharIndex(index); + + // Limit the number of lines moved to the number of lines available to move + // Note lineMax is always >= 1. + int lineMax = _provider.LinesCount; + moved = Math.Min(count, lineMax - line - 1); + + if (moved > 0) + { + // move the endpoint to the beginning of the destination line. + index = _provider.GetLineIndex(line + moved); + } + else if (moved == 0 && lineMax == 1) + { + // There is only one line so get the text length as endpoint. + index = _provider.TextLength; + moved = 1; + } + } + break; + + case TextUnit.Paragraph: + { + // Just like moving words but we look for paragraph boundaries instead of + // word boundaries. + string text = _provider.Text; + ValidateEndpoints(); + moved = 0; + + while (moved < count && index < text.Length) + { + index++; + + while (!AtParagraphBoundary(text, index)) + { + index++; + } + + moved++; + } + } + break; + + case TextUnit.Format: + case TextUnit.Page: + case TextUnit.Document: + { + // Since edit controls are plain text moving one uniform format unit will + // take us all the way to the end of the document, just like + // "pages" and document. + int limit = _provider.TextLength; + ValidateEndpoints(); + + // We'll move 1 format unit if we aren't already at the end of the + // document. Otherwise, we won't move at all. + moved = index < limit ? 1 : 0; + index = limit; + } + break; + + default: + throw new InvalidEnumArgumentException(nameof(unit), (int)unit, typeof(TextUnit)); + } + + return index; + } + + /// + /// Moves an endpoint backward a certain number of units. + /// + private int MoveEndpointBackward(int index, TextUnit unit, int count, out int moved) + { + switch (unit) + { + case TextUnit.Character: + { + ValidateEndpoints(); + int oneBasedIndex = index + 1; + moved = Math.Max(count, -oneBasedIndex); + index = index + moved; + index = index < 0 ? 0 : index; + } + break; + + case TextUnit.Word: + { + string text = _provider.Text; + ValidateEndpoints(); + + for (moved = 0; moved > count && index > 0; moved--) + { + index--; + + while (!AtWordBoundary(text, index)) + { + index--; + } + } + } + break; + + case TextUnit.Line: + { + // Note count < 0. + + // Get 1-based line. + int line = _provider.GetLineFromCharIndex(index) + 1; + + int lineMax = _provider.LinesCount; + + // Truncate the count to the number of available lines. + int actualCount = Math.Max(count, -line); + + moved = actualCount; + + if (actualCount == -line) + { + // We are moving by the maximum number of possible lines, + // so we know the resulting index will be 0. + index = 0; + + // If a line other than the first consists of only "\r\n", + // you can move backwards past this line and the position changes, + // hence this is counted. The first line is special, though: + // if it is empty, and you move say from the second line back up + // to the first, you cannot move further; however if the first line + // is nonempty, you can move from the end of the first line to its + // beginning! This latter move is counted, but if the first line + // is empty, it is not counted. + + // Recalculate the value of "moved". + // The first line is empty if it consists only of + // a line separator sequence. + bool firstLineEmpty = (lineMax == 0 || (lineMax > 1 && _provider.GetLineIndex(1) == LineSeparator.Length)); + if (moved < 0 && firstLineEmpty) + { + ++moved; + } + } + else // actualCount > -line + { + // Move the endpoint to the beginning of the following line, + // then back by the line separator length to get to the end + // of the previous line, since the Edit control has + // no method to get the character index of the end + // of a line directly. + index = _provider.GetLineIndex(line + actualCount) - LineSeparator.Length; + } + } + break; + + case TextUnit.Paragraph: + { + // Just like moving words but we look for paragraph boundaries instead of + // word boundaries. + string text = _provider.Text; + ValidateEndpoints(); + + for (moved = 0; moved > count && index > 0; moved--) + { + index--; + + while (!AtParagraphBoundary(text, index)) + { + index--; + } + } + } + break; + + case TextUnit.Format: + case TextUnit.Page: + case TextUnit.Document: + { + // Since edit controls are plain text moving one uniform format unit will + // take us all the way to the beginning of the document, just like + // "pages" and document. + + // We'll move 1 format unit if we aren't already at the beginning of the + // document. Otherwise, we won't move at all. + moved = index > 0 ? -1 : 0; + index = 0; + } + break; + + default: + throw new InvalidEnumArgumentException(nameof(unit), (int)unit, typeof(TextUnit)); + } + + return index; + } + + /// + /// Method to set both endpoints simultaneously. + /// + private void MoveTo(int start, int end) + { + _start = start >= 0 ? start : throw new ArgumentOutOfRangeException(nameof(start)); + _end = end >= start ? end : throw new ArgumentOutOfRangeException(nameof(end)); + } + + private void ValidateEndpoints() + { + int limit = _provider.TextLength; + + if (Start > limit && limit > 0) + { + Start = limit; + } + + if (End > limit && limit > 0) + { + End = limit; + } + } + } +} diff --git a/src/System.Windows.Forms.Primitives/tests/TestUtilities/TestAccessors.cs b/src/System.Windows.Forms.Primitives/tests/TestUtilities/TestAccessors.cs index b1d4538691b..ea7f459824c 100644 --- a/src/System.Windows.Forms.Primitives/tests/TestUtilities/TestAccessors.cs +++ b/src/System.Windows.Forms.Primitives/tests/TestUtilities/TestAccessors.cs @@ -2,8 +2,6 @@ // 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.Windows.Forms; - namespace System { /// diff --git a/src/System.Windows.Forms.Primitives/tests/TestUtilities/Win32/EditClass.cs b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Win32/EditClass.cs new file mode 100644 index 00000000000..30e24034f0a --- /dev/null +++ b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Win32/EditClass.cs @@ -0,0 +1,15 @@ +// 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; + +namespace System +{ + internal class EditClass : WindowClass + { + public EditClass() : base(ComCtl32.WindowClasses.WC_EDIT) + { + } + } +} diff --git a/src/System.Windows.Forms.Primitives/tests/TestUtilities/Win32/EditControl.cs b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Win32/EditControl.cs new file mode 100644 index 00000000000..92f789b272e --- /dev/null +++ b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Win32/EditControl.cs @@ -0,0 +1,24 @@ +// 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.User32; + +namespace System +{ + internal class EditControl : Window + { + private static readonly EditClass s_editClass = new EditClass(); + + public EditControl(string windowName = default, ES editStyle = ES.LEFT, + WS style = WS.OVERLAPPED, + WS_EX extendedStyle = WS_EX.CLIENTEDGE | WS_EX.LEFT | WS_EX.LTRREADING, + bool isMainWindow = false, Window parentWindow = default, + IntPtr parameters = default, IntPtr menuHandle = default) + : base(s_editClass, new Rectangle(0, 0, 100, 50), windowName, style |= (WS)editStyle, + extendedStyle, isMainWindow, parentWindow, parameters, menuHandle) + { + } + } +} diff --git a/src/System.Windows.Forms.Primitives/tests/TestUtilities/Win32/Window.cs b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Win32/Window.cs new file mode 100644 index 00000000000..bf7c79cc75e --- /dev/null +++ b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Win32/Window.cs @@ -0,0 +1,54 @@ +// 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; + +namespace System +{ + internal class Window : IDisposable, IHandle + { + private readonly WindowClass _windowClass; + + public IntPtr Handle { get; } + + public Window( + WindowClass windowClass, + Rectangle bounds, + string windowName = default, + User32.WS style = User32.WS.OVERLAPPED, + User32.WS_EX extendedStyle = User32.WS_EX.DEFAULT, + bool isMainWindow = false, + Window parentWindow = default, + IntPtr parameters = default, + IntPtr menuHandle = default) + { + _windowClass = windowClass; + if (!_windowClass.IsRegistered) + { + _windowClass.Register(); + } + + Handle = _windowClass.CreateWindow( + bounds, + windowName, + style, + extendedStyle, + isMainWindow, + parentWindow?.Handle ?? default, + parameters, + menuHandle); + } + + public void Dispose() + { + if (Handle != IntPtr.Zero) + { + User32.DestroyWindow(this); + } + + GC.SuppressFinalize(this); + } + } +} diff --git a/src/System.Windows.Forms.Primitives/tests/TestUtilities/WindowClass.cs b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Win32/WindowClass.cs similarity index 77% rename from src/System.Windows.Forms.Primitives/tests/TestUtilities/WindowClass.cs rename to src/System.Windows.Forms.Primitives/tests/TestUtilities/Win32/WindowClass.cs index b1d203c4fdf..08007fcb227 100644 --- a/src/System.Windows.Forms.Primitives/tests/TestUtilities/WindowClass.cs +++ b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Win32/WindowClass.cs @@ -2,7 +2,6 @@ // 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; using System.ComponentModel; using System.Linq; using System.Reflection; @@ -29,7 +28,7 @@ public unsafe static extern IntPtr LoadIconW( private readonly string _className; private readonly string _menuName; - public ushort Atom { get; private set; } + public Atom Atom { get; private set; } public IntPtr MainWindow { get; private set; } public IntPtr ModuleInstance { get; } @@ -111,7 +110,7 @@ public unsafe WindowClass( }; } - public bool IsRegistered => Atom != 0; + public bool IsRegistered => Atom.IsValid || ModuleInstance == IntPtr.Zero; public unsafe WindowClass Register() { @@ -122,11 +121,12 @@ public unsafe WindowClass Register() if (!string.IsNullOrEmpty(_menuName)) _wndClass.lpszMenuName = menuName; - ushort atom = User32.RegisterClassW(ref _wndClass); - if (atom == 0) + Atom atom = User32.RegisterClassW(ref _wndClass); + if (!atom.IsValid) { throw new Win32Exception(); } + Atom = atom; return this; } @@ -165,19 +165,52 @@ public unsafe IntPtr CreateWindow( if (!IsRegistered) throw new ArgumentException("Window class must be registered before using."); - IntPtr window = User32.CreateWindowExW( - dwExStyle: extendedStyle, - lpClassName: (char*)Atom, - lpWindowName: windowName, - dwStyle: style, - X: bounds.X, - Y: bounds.Y, - nWidth: bounds.Width, - nHeight: bounds.Height, - hWndParent: parentWindow, - hMenu: menuHandle, - hInst: IntPtr.Zero, - lpParam: parameters); + IntPtr window; + if (Atom.IsValid) + { + window = User32.CreateWindowExW( + dwExStyle: extendedStyle, + lpClassName: (char*)Atom.ATOM, + lpWindowName: windowName, + dwStyle: style, + X: bounds.X, + Y: bounds.Y, + nWidth: bounds.Width, + nHeight: bounds.Height, + hWndParent: parentWindow, + hMenu: menuHandle, + hInst: IntPtr.Zero, + lpParam: parameters); + } + else + { + fixed (char* atom = _className) + { + window = User32.CreateWindowExW( + dwExStyle: extendedStyle, + lpClassName: atom, + lpWindowName: windowName, + dwStyle: style, + X: bounds.X, + Y: bounds.Y, + nWidth: bounds.Width, + nHeight: bounds.Height, + hWndParent: parentWindow, + hMenu: menuHandle, + hInst: IntPtr.Zero, + lpParam: parameters); + } + } + + if (window == IntPtr.Zero) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if (!Atom.IsValid) + { + Atom = User32.GetClassLong(window, User32.GCL.ATOM); + } if (isMainWindow) MainWindow = window; diff --git a/src/System.Windows.Forms.Primitives/tests/UnitTests/System/Windows/Forms/Automation/TestAccessors.UiaTextRangeTestAccessor.cs b/src/System.Windows.Forms.Primitives/tests/UnitTests/System/Windows/Forms/Automation/TestAccessors.UiaTextRangeTestAccessor.cs new file mode 100644 index 00000000000..950e7335216 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/tests/UnitTests/System/Windows/Forms/Automation/TestAccessors.UiaTextRangeTestAccessor.cs @@ -0,0 +1,71 @@ +// 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.Windows.Forms.Automation; +using static Interop; +using static Interop.User32; + +namespace System +{ + public static partial class TestAccessors + { + internal class UiaTextRangeTestAccessor : TestAccessor + { + // Accessor for static members + private static readonly dynamic Static = typeof(UiaTextRange).TestAccessor().Dynamic; + + public UiaTextRangeTestAccessor(UiaTextRange instance) + : base(instance) { } + + public int _start + { + get => Dynamic._start; + set => Dynamic._start = value; + } + + public int _end + { + get => Dynamic._end; + set => Dynamic._end = value; + } + + public UiaTextProvider _provider => Dynamic._provider; + + public CapStyle GetCapStyle(ES editStyle) => Dynamic.GetCapStyle(editStyle); + + public double GetFontSize(LOGFONTW logfont) => Dynamic.GetFontSize(logfont); + + public HorizontalTextAlignment GetHorizontalTextAlignment(ES editStyle) => Dynamic.GetHorizontalTextAlignment(editStyle); + + public bool GetReadOnly() => Dynamic.GetReadOnly(); + + public void MoveTo(int start, int end) => Dynamic.MoveTo(start, end); + + public void ValidateEndpoints() => Dynamic.ValidateEndpoints(); + + public bool AtParagraphBoundary(string text, int index) => Static.AtParagraphBoundary(text, index); + + public bool AtWordBoundary(string text, int index) => Static.AtWordBoundary(text, index); + + public COLORREF GetBackgroundColor() => Static.GetBackgroundColor(); + + public string GetFontName(LOGFONTW logfont) => Static.GetFontName(logfont); + + public bool IsApostrophe(char ch) => Static.IsApostrophe(ch); + + public Gdi32.FW GetFontWeight(LOGFONTW logfont) => Static.GetFontWeight(logfont); + + public COLORREF GetForegroundColor() => Static.GetForegroundColor(); + + public bool GetItalic(LOGFONTW logfont) => Static.GetItalic(logfont); + + public TextDecorationLineStyle GetStrikethroughStyle(LOGFONTW logfont) => Static.GetStrikethroughStyle(logfont); + + public TextDecorationLineStyle GetUnderlineStyle(LOGFONTW logfont) => Static.GetUnderlineStyle(logfont); + } + + internal static UiaTextRangeTestAccessor TestAccessor(this UiaTextRange textRange) + => new UiaTextRangeTestAccessor(textRange); + } +} diff --git a/src/System.Windows.Forms.Primitives/tests/UnitTests/System/Windows/Forms/Automation/UiaTextProviderTests.cs b/src/System.Windows.Forms.Primitives/tests/UnitTests/System/Windows/Forms/Automation/UiaTextProviderTests.cs new file mode 100644 index 00000000000..e51465e329f --- /dev/null +++ b/src/System.Windows.Forms.Primitives/tests/UnitTests/System/Windows/Forms/Automation/UiaTextProviderTests.cs @@ -0,0 +1,150 @@ +// 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 System.Windows.Forms.Automation; +using Moq; +using Xunit; +using static Interop.User32; + +namespace System.Windows.Forms.Primitives.Tests.Automation +{ + public class UiaTextProviderTests + { + [StaFact] + public void UiaTextProvider_GetEditStyle_ContainsMultilineStyle_ForMultilineTextBox() + { + // EditControl Multiline is true when EditControl has ES_MULTILINE style + using EditControl textBox = new EditControl( + editStyle: ES.MULTILINE | ES.LEFT | ES.AUTOHSCROLL | ES.AUTOVSCROLL, + style: WS.OVERLAPPED | WS.VISIBLE); + Mock providerMock = new Mock(MockBehavior.Strict); + + ES actual = providerMock.Object.GetEditStyle(textBox.Handle); + Assert.True(actual.HasFlag(ES.MULTILINE)); + } + + [StaFact] + public void UiaTextProvider_GetEditStyle_DoesntContainMultilineStyle_ForSinglelineTextBox() + { + // EditControl Multiline is false when EditControl doesn't have ES_MULTILINE style + using EditControl textBox = new EditControl( + editStyle: ES.LEFT | ES.AUTOHSCROLL | ES.AUTOVSCROLL, + style: WS.OVERLAPPED | WS.VISIBLE); + Mock providerMock = new Mock(MockBehavior.Strict); + + ES actual = providerMock.Object.GetEditStyle(textBox.Handle); + Assert.False(actual.HasFlag(ES.MULTILINE)); + } + + [StaFact] + public void UiaTextProvider_GetEditStyle_ReturnsLeft_WithoutHandle() + { + Mock providerMock = new Mock(MockBehavior.Strict); + + ES actual = providerMock.Object.GetEditStyle(IntPtr.Zero); + Assert.Equal(ES.LEFT, actual); + } + + [StaFact] + public void UiaTextProvider_GetWindowStyle_ContainsVisible() + { + using EditControl textBox = new EditControl( + editStyle: ES.MULTILINE | ES.LEFT | ES.AUTOHSCROLL | ES.AUTOVSCROLL, + style: WS.OVERLAPPED | WS.VISIBLE); + Mock providerMock = new Mock(MockBehavior.Strict); + + WS actual = providerMock.Object.GetWindowStyle(textBox.Handle); + Assert.True(actual.HasFlag(WS.VISIBLE)); + } + + [StaFact] + public void UiaTextProvider_GetWindowStyle_ReturnsDisabled_WithoutHandle() + { + Mock providerMock = new Mock(MockBehavior.Strict); + + WS actual = providerMock.Object.GetWindowStyle(IntPtr.Zero); + Assert.Equal(WS.DISABLED, actual); + } + + [StaFact] + public void UiaTextProvider_GetWindowExStyle_ContainsClientedge() + { + using EditControl textBox = new EditControl( + style: WS.OVERLAPPED | WS.VISIBLE); + Mock providerMock = new Mock(MockBehavior.Strict); + + WS_EX actual = providerMock.Object.GetWindowExStyle(textBox.Handle); + Assert.True(actual.HasFlag(WS_EX.CLIENTEDGE)); + } + + [StaFact] + public void UiaTextProvider_GetWindowExStyle_ReturnsLeft_WithoutHandle() + { + Mock providerMock = new Mock(MockBehavior.Strict); + WS_EX actual = providerMock.Object.GetWindowExStyle(IntPtr.Zero); + Assert.Equal(WS_EX.LEFT, actual); + } + + [StaFact] + public void UiaTextProvider_RectArrayToDoubleArray_ReturnsCorrectValue() + { + Mock providerMock = new Mock(MockBehavior.Strict); + + double[] expected = { 0, 0, 10, 5, 10, 10, 20, 30 }; + double[] actual = providerMock.Object.RectListToDoubleArray(new List + { + new Rectangle(0, 0, 10, 5), + new Rectangle(10, 10, 20, 30) + }); + + Assert.Equal(8, actual.Length); + + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], actual[i]); + } + } + +#pragma warning disable CS8625 // RectArrayToDoubleArray doesn't accept a null parameter + [StaFact] + public void UiaTextProvider_RectArrayToDoubleArray_NullParameter_ReturnsNull() + { + Mock providerMock = new Mock(MockBehavior.Strict); + + double[] actual = providerMock.Object.RectListToDoubleArray(null); + Assert.Empty(actual); + } +#pragma warning restore CS8625 + + [StaFact] + public void UiaTextProvider_RectArrayToDoubleArray_EmptyArrayParameter_ReturnsEmptyArrayResult() + { + Mock providerMock = new Mock(MockBehavior.Strict); + + double[] actual = providerMock.Object.RectListToDoubleArray(new List()); + Assert.Empty(actual); + } + + [StaFact] + public unsafe void UiaTextProvider_SendInput_SendsOneInput() + { + Mock providerMock = new Mock(MockBehavior.Strict); + INPUT keyboardInput = new INPUT(); + + int actual = providerMock.Object.SendInput(1, ref keyboardInput, sizeof(INPUT)); + Assert.Equal(1, actual); + } + + [StaFact] + public void UiaTextProvider_SendKeyboardInputVK_SendsOneInput() + { + Mock providerMock = new Mock(MockBehavior.Strict); + + int actual = providerMock.Object.SendKeyboardInputVK(VK.LEFT, true); + Assert.Equal(1, actual); + } + } +} diff --git a/src/System.Windows.Forms.Primitives/tests/UnitTests/System/Windows/Forms/Automation/UiaTextRangeTests.cs b/src/System.Windows.Forms.Primitives/tests/UnitTests/System/Windows/Forms/Automation/UiaTextRangeTests.cs new file mode 100644 index 00000000000..ff1c6e65365 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/tests/UnitTests/System/Windows/Forms/Automation/UiaTextRangeTests.cs @@ -0,0 +1,1106 @@ +// 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 System.Windows.Forms.Automation; +using Xunit; +using static Interop; +using static Interop.UiaCore; +using static Interop.Gdi32; +using System.Runtime.InteropServices; +using Moq; +using static Interop.User32; + +namespace System.Windows.Forms.Primitives.Tests.Automation +{ + public class UiaTextRangeTests + { + // Used to get access to the test accessor for static members + private const UiaTextRange StaticNullTextRange = null!; + + [StaTheory] + [InlineData(0, 0)] + [InlineData(0, 5)] + [InlineData(5, 10)] + [InlineData(1, 1)] + public void UiaTextRange_Constructor_InitializesProvider_And_CorrectEndpoints(int start, int end) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start, end); + Assert.Equal(start, textRange.Start); + Assert.Equal(end, textRange.End); + Assert.Equal(enclosingElement, ((ITextRangeProvider)textRange).GetEnclosingElement()); + + object actual = textRange.TestAccessor()._provider; + + Assert.Equal(provider, actual); + } + + [StaTheory] + [InlineData(-10, 0, 0, 0)] + [InlineData(0, -10, 0, 0)] + [InlineData(5, 0, 5, 5)] + [InlineData(-1, -1, 0, 0)] + [InlineData(10, 5, 10, 10)] + [InlineData(-5, 5, 0, 5)] + [InlineData(5, -5, 5, 5)] + public void UiaTextRange_Constructor_InitializesProvider_And_CorrectEndpoints_IfEndpointsincorrect(int start, int end, int expectedStart, int expectedEnd) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start, end); + Assert.Equal(expectedStart, textRange.Start); + Assert.Equal(expectedEnd, textRange.End); + } + +#pragma warning disable CS8625 // UiaTextRange constructor doesn't accept a provider null parameter + [StaFact] + public void UiaTextRange_Constructor_Provider_Null_ThrowsException() + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + Assert.Throws(() => new UiaTextRange(enclosingElement, null, 0, 5)); + } +#pragma warning restore CS8625 + +#pragma warning disable CS8625 // UiaTextRange constructor doesn't accept an enclosingElement null parameter + [StaFact] + public void UiaTextRange_Constructor_Control_Null_ThrowsException() + { + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + Assert.Throws(() => new UiaTextRange(null, provider, 0, 5)); + } +#pragma warning restore CS8625 + + [StaTheory] + [InlineData(3, -5)] + [InlineData(-5, 3)] + [InlineData(-3, -5)] + public void UiaTextRange_Constructor_SetCorrectValues_IfNegativeStartEnd(int start, int end) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start, end); + Assert.True(textRange.Start >= 0); + Assert.True(textRange.End >= 0); + } + + [StaTheory] + [InlineData(0)] + [InlineData(5)] + [InlineData(int.MaxValue)] + public void UiaTextRange_End_Get_ReturnsCorrectValue(int end) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start: 0, end); + Assert.Equal(end, textRange.End); + } + + [StaTheory] + [InlineData(0)] + [InlineData(5)] + [InlineData(int.MaxValue)] + public void UiaTextRange_End_SetCorrectly(int end) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start: 0, end: 0); + textRange.End = end; + int actual = textRange.End < textRange.Start ? textRange.Start : textRange.End; + Assert.Equal(end, actual); + } + + [StaFact] + public void UiaTextRange_End_SetCorrect_IfValueIncorrect() + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start: 5, end: 10); + textRange.End = 3; /*Incorrect value*/ + Assert.Equal(textRange.Start, textRange.End); + + textRange.End = 6; + Assert.Equal(6, textRange.End); + + textRange.End = -10; /*Incorrect value*/ + Assert.Equal(textRange.Start, textRange.End); + } + + [StaTheory] + [InlineData(0, 0, 0)] + [InlineData(5, 5, 0)] + [InlineData(3, 15, 12)] + [InlineData(0, 10, 10)] + [InlineData(6, 10, 4)] + public void UiaTextRange_Length_ReturnsCorrectValue(int start, int end, int expected) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start, end); + Assert.Equal(expected, textRange.Length); + } + + [StaTheory] + [InlineData(-5, 0)] + [InlineData(0, -5)] + [InlineData(-5, -5)] + [InlineData(10, 5)] + public void UiaTextRange_Length_ReturnsCorrectValue_IfIncorrectStartEnd(int start, int end) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, 3, 10); + + var testAccessor = textRange.TestAccessor(); + testAccessor._start = start; + testAccessor._end = end; + + Assert.Equal(0, textRange.Length); + } + + [StaTheory] + [InlineData(0)] + [InlineData(5)] + [InlineData(int.MaxValue)] + public void UiaTextRange_Start_Get_ReturnsCorrectValue(int start) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start: 0, end: 0); + + textRange.TestAccessor()._start = start; + + Assert.Equal(start, textRange.Start); + } + + [StaTheory] + [InlineData(0)] + [InlineData(5)] + [InlineData(int.MaxValue)] + public void UiaTextRange_Start_SetCorrectly(int start) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start: 0, end: 0); + textRange.Start = start; + int actual = textRange.Start < textRange.End ? textRange.End : textRange.Start; + Assert.Equal(start, actual); + } + + [StaFact] + public void UiaTextRange_Start_Set_Correct_IfValueIncorrect() + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start: 4, end: 8); + textRange.Start = -10; + Assert.Equal(0, textRange.Start); + Assert.Equal(8, textRange.End); + } + + [StaFact] + public void UiaTextRange_Start_Set_Correct_IfValueMoreThanEnd() + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start: 4, end: 10); + textRange.Start = 15; // More than End = 10 + Assert.True(textRange.Start <= textRange.End); + } + + [StaFact] + public void UiaTextRange_ITextRangeProvider_Clone_ReturnsCorrectValue() + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start: 3, end: 9); + UiaTextRange actual = (UiaTextRange)((ITextRangeProvider)textRange).Clone(); + Assert.Equal(textRange.Start, actual.Start); + Assert.Equal(textRange.End, actual.End); + } + + [StaTheory] + [InlineData(3, 9, true)] + [InlineData(0, 2, false)] + public void UiaTextRange_ITextRangeProvider_Compare_ReturnsCorrectValue(int start, int end, bool expected) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange1 = new UiaTextRange(enclosingElement, provider, start: 3, end: 9); + UiaTextRange textRange2 = new UiaTextRange(enclosingElement, provider, start, end); + bool actual = ((ITextRangeProvider)textRange1).Compare(textRange2).IsTrue(); + Assert.Equal(expected, actual); + } + + public static IEnumerable UiaTextRange_ITextRangeProvider_CompareEndpoints_ReturnsCorrectValue_TestData() + { + yield return new object[] { TextPatternRangeEndpoint.Start, 3, 9, TextPatternRangeEndpoint.Start, 0 }; + yield return new object[] { TextPatternRangeEndpoint.End, 3, 9, TextPatternRangeEndpoint.Start, 6 }; + yield return new object[] { TextPatternRangeEndpoint.Start, 3, 9, TextPatternRangeEndpoint.End, -6 }; + yield return new object[] { TextPatternRangeEndpoint.End, 3, 9, TextPatternRangeEndpoint.End, 0 }; + yield return new object[] { TextPatternRangeEndpoint.Start, 0, 0, TextPatternRangeEndpoint.Start, 3 }; + yield return new object[] { TextPatternRangeEndpoint.End, 0, 0, TextPatternRangeEndpoint.Start, 9 }; + yield return new object[] { TextPatternRangeEndpoint.End, 1, 15, TextPatternRangeEndpoint.End, -6 }; + yield return new object[] { TextPatternRangeEndpoint.Start, 1, 15, TextPatternRangeEndpoint.End, -12 }; + } + + [StaTheory] + [MemberData(nameof(UiaTextRange_ITextRangeProvider_CompareEndpoints_ReturnsCorrectValue_TestData))] + public void UiaTextRange_ITextRangeProvider_CompareEndpoints_ReturnsCorrectValue( + int endpoint, + int targetStart, + int targetEnd, + int targetEndpoint, + int expected) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start: 3, end: 9); + UiaTextRange targetRange = new UiaTextRange(enclosingElement, provider, start: targetStart, end: targetEnd); + int actual = ((ITextRangeProvider)textRange).CompareEndpoints((TextPatternRangeEndpoint)endpoint, targetRange, (TextPatternRangeEndpoint)targetEndpoint); + Assert.Equal(expected, actual); + } + + [StaTheory] + [InlineData(2, 2, 2, 3)] + [InlineData(8, 9, 8, 9)] + [InlineData(0, 3, 0, 3)] + public void UiaTextRange_ITextRangeProvider_ExpandToEnclosingUnit_ExpandsToCharacter(int start, int end, int expandedStart, int expandedEnd) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + Mock providerMock = new Mock(MockBehavior.Strict); + providerMock.Setup(m => m.TextLength).Returns("words, words, words".Length); + UiaTextProvider provider = providerMock.Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start, end); + ((ITextRangeProvider)textRange).ExpandToEnclosingUnit(TextUnit.Character); + Assert.Equal(expandedStart, textRange.Start); + Assert.Equal(expandedEnd, textRange.End); + } + + [StaTheory] + [InlineData(2, 3, 0, 5)] + [InlineData(8, 8, 7, 12)] + [InlineData(16, 17, 14, 19)] + public void UiaTextRange_ITextRangeProvider_ExpandToEnclosingUnit_ExpandsToWord(int start, int end, int expandedStart, int expandedEnd) + { + string testText = "words, words, words"; + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + Mock providerMock = new Mock(MockBehavior.Strict); + providerMock.Setup(m => m.Text).Returns(testText); + providerMock.Setup(m => m.TextLength).Returns(testText.Length); + UiaTextProvider provider = providerMock.Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start, end); + ((ITextRangeProvider)textRange).ExpandToEnclosingUnit(TextUnit.Word); + Assert.Equal(expandedStart, textRange.Start); + Assert.Equal(expandedEnd, textRange.End); + } + + [StaTheory] + [InlineData(2, 4, 0, 12)] + [InlineData(15, 16, 12, 25)] + [InlineData(27, 28, 25, 36)] + public void UiaTextRange_ITextRangeProvider_ExpandToEnclosingUnit_ExpandsToLine(int start, int end, int expandedStart, int expandedEnd) + { + string testText = +@"First line +second line +third line."; + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + Mock providerMock = new Mock(MockBehavior.Strict); + providerMock.Setup(m => m.Text).Returns(testText); + providerMock.Setup(m => m.TextLength).Returns(testText.Length); + providerMock.Setup(m => m.LinesCount).Returns(3); + providerMock.Setup(m => m.GetLineIndex(0)).Returns(0); + providerMock.Setup(m => m.GetLineIndex(1)).Returns(12); + providerMock.Setup(m => m.GetLineIndex(2)).Returns(25); + providerMock.Setup(m => m.GetLineFromCharIndex(2)).Returns(0); + providerMock.Setup(m => m.GetLineFromCharIndex(4)).Returns(0); + providerMock.Setup(m => m.GetLineFromCharIndex(15)).Returns(1); + providerMock.Setup(m => m.GetLineFromCharIndex(16)).Returns(1); + providerMock.Setup(m => m.GetLineFromCharIndex(27)).Returns(2); + providerMock.Setup(m => m.GetLineFromCharIndex(28)).Returns(2); + UiaTextProvider provider = providerMock.Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start, end); + ((ITextRangeProvider)textRange).ExpandToEnclosingUnit(TextUnit.Line); + Assert.Equal(expandedStart, textRange.Start); + Assert.Equal(expandedEnd, textRange.End); + } + + [StaTheory] + [InlineData(2, 4, 0, 24)] + [InlineData(30, 30, 24, 49)] + [InlineData(49, 60, 49, 72)] + public void UiaTextRange_ITextRangeProvider_ExpandToEnclosingUnit_ExpandsToParagraph(int start, int end, int expandedStart, int expandedEnd) + { + string testText = +@"This is the first line +this is the second line +this is the third line."; + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + Mock providerMock = new Mock(MockBehavior.Strict); + providerMock.Setup(m => m.Text).Returns(testText); + providerMock.Setup(m => m.TextLength).Returns(testText.Length); + UiaTextProvider provider = providerMock.Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start, end); + ((ITextRangeProvider)textRange).ExpandToEnclosingUnit(TextUnit.Paragraph); + Assert.Equal(expandedStart, textRange.Start); + Assert.Equal(expandedEnd, textRange.End); + } + + public static IEnumerable UiaTextRange_ITextRangeProvider_ExpandToEnclosingUnit_ExpandsToAllText_TestData() + { + yield return new object[] { 5, 8, TextUnit.Page, 0, 72 }; + yield return new object[] { 10, 10, TextUnit.Format, 0, 72 }; + yield return new object[] { 10, 10, TextUnit.Document, 0, 72 }; + } + + [StaTheory] + [MemberData(nameof(UiaTextRange_ITextRangeProvider_ExpandToEnclosingUnit_ExpandsToAllText_TestData))] + internal void UiaTextRange_ITextRangeProvider_ExpandToEnclosingUnit_ExpandsToAllText(int start, int end, TextUnit textUnit, int expandedStart, int expandedEnd) + { + string testText = +@"This is the first line +this is the second line +this is the third line."; + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + Mock providerMock = new Mock(MockBehavior.Strict); + providerMock.Setup(m => m.TextLength).Returns(testText.Length); + UiaTextProvider provider = providerMock.Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start, end); + ((ITextRangeProvider)textRange).ExpandToEnclosingUnit(textUnit); + Assert.Equal(expandedStart, textRange.Start); + Assert.Equal(expandedEnd, textRange.End); + } + + [StaTheory] + [InlineData(true)] + [InlineData(false)] + internal void UiaTextRange_ITextRangeProvider_FindAttribute_Returns_null(bool backward) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start: 0, end: 0); + Array textAttributeIdentifiers = Enum.GetValues(typeof(TextAttributeIdentifier)); + + foreach (int textAttributeIdentifier in textAttributeIdentifiers) + { + ITextRangeProvider? actual = ((ITextRangeProvider)textRange).FindAttribute(textAttributeIdentifier, new object(), backward.ToBOOL()); + Assert.Null(actual); + } + } + + public static IEnumerable UiaTextRange_ITextRangeProvider_FindText_Returns_Correct_TestData() + { + yield return new object?[] { "text", "text", BOOL.FALSE, BOOL.FALSE }; + yield return new object?[] { "other", null, BOOL.FALSE, BOOL.FALSE }; + yield return new object?[] { "TEXT", "text", BOOL.FALSE, BOOL.TRUE }; + yield return new object?[] { "TEXT", null, BOOL.FALSE, BOOL.FALSE }; + } + + [StaTheory] + [MemberData(nameof(UiaTextRange_ITextRangeProvider_FindText_Returns_Correct_TestData))] + internal void UiaTextRange_ITextRangeProvider_FindText_Returns_Correct(string textToSearch, string? foundText, BOOL backward, BOOL ignoreCase) + { + string testText = "Test text to find something."; + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + Mock providerMock = new Mock(MockBehavior.Strict); + providerMock.Setup(m => m.Text).Returns(testText); + providerMock.Setup(m => m.TextLength).Returns(testText.Length); + UiaTextProvider provider = providerMock.Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start: 0, end: 28); + + ITextRangeProvider? actual = ((ITextRangeProvider)textRange).FindText(textToSearch, backward, ignoreCase); + + if (foundText != null) + { + Assert.Equal(foundText, actual?.GetText(5000)); + } + else + { + Assert.Null(actual); + } + } + +#pragma warning disable CS8625 // FindText doesn't accept a text null parameter + [StaFact] + internal void UiaTextRange_ITextRangeProvider_FindText_ReturnsNull_IfTextNull() + { + using (new NoAssertContext()) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start: 0, end: 28); + ITextRangeProvider? actual = ((ITextRangeProvider)textRange).FindText(null, BOOL.TRUE, BOOL.TRUE); + Assert.Null(actual); + } + } +#pragma warning restore CS8625 + + private static object? notSupportedValue; + + [DllImport(Libraries.UiaCore, ExactSpelling = true)] + private static extern int UiaGetReservedNotSupportedValue([MarshalAs(UnmanagedType.IUnknown)] out object notSupportedValue); + + public static object UiaGetReservedNotSupportedValue() + { + if (notSupportedValue == null) + { + UiaGetReservedNotSupportedValue(out notSupportedValue); + } + + return notSupportedValue; + } + + public static IEnumerable UiaTextRange_ITextRangeProvider_GetAttributeValue_Returns_Correct_TestData() + { + yield return new object[] { TextAttributeIdentifier.BackgroundColorAttributeId, GetSysColor(COLOR.WINDOW) }; + yield return new object[] { TextAttributeIdentifier.CapStyleAttributeId, CapStyle.None }; + yield return new object[] { TextAttributeIdentifier.FontNameAttributeId, "Segoe UI" }; + yield return new object[] { TextAttributeIdentifier.FontSizeAttributeId, 9.0 }; + yield return new object[] { TextAttributeIdentifier.FontWeightAttributeId, FW.NORMAL }; + yield return new object[] { TextAttributeIdentifier.ForegroundColorAttributeId, new COLORREF() }; + yield return new object[] { TextAttributeIdentifier.HorizontalTextAlignmentAttributeId, HorizontalTextAlignment.Left }; + yield return new object[] { TextAttributeIdentifier.IsItalicAttributeId, false }; + yield return new object[] { TextAttributeIdentifier.IsReadOnlyAttributeId, false }; + yield return new object[] { TextAttributeIdentifier.StrikethroughStyleAttributeId, TextDecorationLineStyle.None }; + yield return new object[] { TextAttributeIdentifier.UnderlineStyleAttributeId, TextDecorationLineStyle.None }; + + yield return new object[] { TextAttributeIdentifier.AnimationStyleAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.BulletStyleAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.CultureAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.IndentationFirstLineAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.IndentationLeadingAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.IndentationTrailingAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.IsHiddenAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.IsSubscriptAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.IsSuperscriptAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.MarginBottomAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.MarginLeadingAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.MarginTopAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.MarginTrailingAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.OutlineStylesAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.OverlineColorAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.OverlineStyleAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.StrikethroughColorAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.TabsAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.TextFlowDirectionsAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.UnderlineColorAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.AnnotationTypesAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.AnnotationObjectsAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.StyleNameAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.StyleIdAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.LinkAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.IsActiveAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.SelectionActiveEndAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.CaretPositionAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.CaretBidiModeAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.LineSpacingAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.BeforeParagraphSpacingAttributeId, UiaGetReservedNotSupportedValue() }; + yield return new object[] { TextAttributeIdentifier.AfterParagraphSpacingAttributeId, UiaGetReservedNotSupportedValue() }; + } + + [StaTheory] + [MemberData(nameof(UiaTextRange_ITextRangeProvider_GetAttributeValue_Returns_Correct_TestData))] + internal void UiaTextRange_ITextRangeProvider_GetAttributeValue_Returns_Correct(int attributeId, object attributeValue) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + Mock providerMock = new Mock(MockBehavior.Strict); + using Font font = new Font("Segoe UI", 9, FontStyle.Regular); + providerMock.Setup(m => m.Logfont).Returns(LOGFONTW.FromFont(font)); + providerMock.Setup(m => m.EditStyle).Returns(User32.ES.LEFT); + providerMock.Setup(m => m.IsReadOnly).Returns(false); + UiaTextProvider provider = providerMock.Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start: 0, end: 28); + object? actual = ((ITextRangeProvider)textRange).GetAttributeValue(attributeId); + Assert.Equal(attributeValue, actual); + } + + [StaFact] + public void UiaTextRange_ITextRangeProvider_GetBoundingRectangles_ReturnsEmpty_for_DegenerateRange() + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + Mock providerMock = new Mock(MockBehavior.Strict); + providerMock.Setup(p => p.TextLength).Returns(5); + UiaTextProvider provider = providerMock.Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start: 0, end: 0); + var actual = ((ITextRangeProvider)textRange).GetBoundingRectangles(); + Assert.Empty(actual); + } + + [StaFact] + public void UiaTextRange_ITextRangeProvider_GetBoundingRectangles_ReturnsExpected_for_Endline() + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + Mock providerMock = new Mock(MockBehavior.Strict); + providerMock.Setup(p => p.TextLength).Returns(3); + providerMock.Setup(p => p.PointToScreen(It.IsAny())).Returns(Point.Empty); + using Font font = new Font("Arial", 9f, FontStyle.Regular); + providerMock.Setup(m => m.Logfont).Returns(LOGFONTW.FromFont(font)); + UiaTextProvider provider = providerMock.Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start: 3, end: 3); + double actualWidth = ((ITextRangeProvider)textRange).GetBoundingRectangles()[2]; // {X,Y,Width,Height} + Assert.Equal(actualWidth, UiaTextProvider.EndOfLineWidth); + } + + public static IEnumerable UiaTextRange_ITextRangeProvider_GetBoundingRectangles_ReturnsCorrectValue_for_SingleLine_TestData() + { + yield return new object[] { 3, 6, new double[] { 27, 34, 11, 14 } }; + yield return new object[] { 0, 2, new double[] { 11, 34, 16, 15 } }; + } + + [StaTheory] + [MemberData(nameof(UiaTextRange_ITextRangeProvider_GetBoundingRectangles_ReturnsCorrectValue_for_SingleLine_TestData))] + public void UiaTextRange_ITextRangeProvider_GetBoundingRectangles_ReturnsCorrectValue_for_SingleLine(int start, int end, double[] expected) + { + string testText = "Test text."; + Mock enclosingElementMock = new Mock(MockBehavior.Strict); + enclosingElementMock.Setup(m => m.GetPropertyValue(UIA.BoundingRectanglePropertyId)).Returns(new Rectangle(10, 33, 96, 19)); + IRawElementProviderSimple enclosingElement = enclosingElementMock.Object; + Mock providerMock = new Mock(MockBehavior.Strict); + providerMock.Setup(m => m.Text).Returns(testText); + providerMock.Setup(m => m.TextLength).Returns(testText.Length); + providerMock.Setup(m => m.IsMultiline).Returns(false); + providerMock.Setup(m => m.BoundingRectangle).Returns(new Rectangle(1, 1, 94, 15)); + providerMock.Setup(m => m.GetPositionFromChar(3)).Returns(new Point(17, 0)); + providerMock.Setup(m => m.GetPositionFromChar(0)).Returns(new Point(1, 0)); + providerMock.Setup(m => m.GetPositionFromCharForUpperRightCorner(5, testText)).Returns(new Point(28, 0)); + providerMock.Setup(m => m.GetPositionFromCharForUpperRightCorner(1, testText)).Returns(new Point(17, 0)); + UiaTextProvider provider = providerMock.Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start, end); + double[] actual = ((ITextRangeProvider)textRange).GetBoundingRectangles(); + + // Acceptable deviation of 1 px. + for (int i = 0; i < actual.Length; i++) + { + Assert.True(actual[i] >= 0 && actual[i] >= expected[i] - 1 && actual[i] <= expected[i] + 1); + } + } + + public static IEnumerable UiaTextRange_ITextRangeProvider_GetBoundingRectangles_ReturnsCorrectValue_for_MultiLine_TestData() + { + yield return new object[] { 18, 30, new double[] { 14, 51, 12, 13, 14, 66, 52, 13 } }; + yield return new object[] { 32, 35, new double[] { 74, 66, 20, 13 } }; + } + + [StaTheory] + [MemberData(nameof(UiaTextRange_ITextRangeProvider_GetBoundingRectangles_ReturnsCorrectValue_for_MultiLine_TestData))] + public void UiaTextRange_ITextRangeProvider_GetBoundingRectangles_ReturnsCorrectValue_for_MultiLine(int start, int end, double[] expected) + { + string testText = +@"Test text on line 1. +Test text on line 2."; + Mock enclosingElementMock = new Mock(MockBehavior.Strict); + enclosingElementMock.Setup(m => m.GetPropertyValue(UIA.BoundingRectanglePropertyId)).Returns(new Rectangle(10, 33, 96, 56)); + IRawElementProviderSimple enclosingElement = enclosingElementMock.Object; + Mock providerMock = new Mock(MockBehavior.Strict); + using Font font = new Font("Arial", 9f, FontStyle.Regular); + providerMock.Setup(m => m.Logfont).Returns(LOGFONTW.FromFont(font)); + providerMock.Setup(m => m.Text).Returns(testText); + providerMock.Setup(m => m.TextLength).Returns(testText.Length); + providerMock.Setup(m => m.IsMultiline).Returns(true); + providerMock.Setup(m => m.BoundingRectangle).Returns(new Rectangle(4, 1, 88, 45)); + providerMock.Setup(m => m.GetLineFromCharIndex(18)).Returns(1); + providerMock.Setup(m => m.GetLineFromCharIndex(29)).Returns(2); + providerMock.Setup(m => m.GetLineFromCharIndex(32)).Returns(2); + providerMock.Setup(m => m.GetLineFromCharIndex(34)).Returns(2); + providerMock.Setup(m => m.FirstVisibleLine).Returns(0); + providerMock.Setup(m => m.LinesPerPage).Returns(3); + providerMock.Setup(m => m.GetLineIndex(1)).Returns(18); + providerMock.Setup(m => m.GetPositionFromChar(18)).Returns(new Point(4, 16)); + providerMock.Setup(m => m.GetPositionFromChar(32)).Returns(new Point(64, 31)); + providerMock.Setup(m => m.GetLineIndex(2)).Returns(22); + providerMock.Setup(m => m.GetPositionFromChar(21)).Returns(new Point(16, 16)); + providerMock.Setup(m => m.GetPositionFromChar(22)).Returns(new Point(4, 31)); + providerMock.Setup(m => m.GetPositionFromCharForUpperRightCorner(29, testText)).Returns(new Point(56, 31)); + providerMock.Setup(m => m.GetPositionFromCharForUpperRightCorner(34, testText)).Returns(new Point(84, 31)); + UiaTextProvider provider = providerMock.Object; + var t = provider.Logfont; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start, end); + var actual = ((ITextRangeProvider)textRange).GetBoundingRectangles(); + Assert.Equal(expected, actual); + } + + [StaFact] + public void UiaTextRange_ITextRangeProvider_GetEnclosingElement_ReturnsCorrectValue() + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start: 0, end: 0); + IRawElementProviderSimple actual = ((ITextRangeProvider)textRange).GetEnclosingElement(); + Assert.Equal(enclosingElement, actual); + } + + [StaTheory] + [InlineData(0, 0, 0, "")] + [InlineData(0, 0, 5, "")] + [InlineData(0, 10, -5, "Some long ")] + [InlineData(0, 10, 0, "")] + [InlineData(0, 10, 10, "Some long ")] + [InlineData(0, 10, 20, "Some long ")] + [InlineData(0, 25, 7, "Some lo")] + [InlineData(0, 300, 400, "Some long long test text")] + [InlineData(5, 15, 7, "long lo")] + [InlineData(5, 15, 25, "long long ")] + [InlineData(5, 15, 300, "long long ")] + [InlineData(5, 24, 400, "long long test text")] + [InlineData(5, 25, 0, "")] + [InlineData(5, 25, 7, "long lo")] + [InlineData(5, 300, -5, "long long test text")] + [InlineData(5, 300, 7, "long lo")] + [InlineData(5, 300, 300, "long long test text")] + public void UiaTextRange_ITextRangeProvider_GetText_ReturnsCorrectValue(int start, int end, int maxLength, string expected) + { + string testText = "Some long long test text"; + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + Mock providerMock = new Mock(MockBehavior.Strict); + providerMock.Setup(m => m.Text).Returns(testText); + providerMock.Setup(m => m.TextLength).Returns(testText.Length); + UiaTextProvider provider = providerMock.Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start, end); + string actual = ((ITextRangeProvider)textRange).GetText(maxLength); + Assert.Equal(expected, actual); + } + + public static IEnumerable UiaTextRange_ITextRangeProvider_Move_MovesCorrectly_TestData() + { + yield return new object[] { 0, 5, TextUnit.Character, 1, 6, 6 }; + yield return new object[] { 1, 6, TextUnit.Character, 5, 11, 11 }; + yield return new object[] { 0, 5, TextUnit.Character, -2, 0, 0 }; + yield return new object[] { 3, 6, TextUnit.Character, -2, 1, 1 }; + yield return new object[] { 1, 2, TextUnit.Word, 1, 4, 4 }; + yield return new object[] { 1, 2, TextUnit.Word, 5, 11, 11 }; + yield return new object[] { 12, 14, TextUnit.Word, -2, 8, 8 }; + yield return new object[] { 12, 14, TextUnit.Word, -10, 0, 0 }; + } + + [StaTheory] + [MemberData(nameof(UiaTextRange_ITextRangeProvider_Move_MovesCorrectly_TestData))] + internal void UiaTextRange_ITextRangeProvider_Move_MovesCorrectly(int start, int end, TextUnit unit, int count, int expectedStart, int expectedEnd) + { + string testText = +@"This is the text to move on - line 1 +This is the line 2 +This is the line 3"; + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + Mock providerMock = new Mock(MockBehavior.Strict); + providerMock.Setup(m => m.Text).Returns(testText); + providerMock.Setup(m => m.TextLength).Returns(testText.Length); + UiaTextProvider provider = providerMock.Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start, end); + int result = ((ITextRangeProvider)textRange).Move(unit, count); + Assert.Equal(expectedStart, textRange.Start); + Assert.Equal(expectedEnd, textRange.End); + } + + public static IEnumerable UiaTextRange_ITextRangeProvider_MoveEndpointByUnit_MovesCorrectly_TestData() + { + yield return new object[] { 0, 5, TextPatternRangeEndpoint.Start, TextUnit.Character, 1, 1, 5 }; + yield return new object[] { 1, 6, TextPatternRangeEndpoint.Start, TextUnit.Character, 5, 6, 6 }; + yield return new object[] { 0, 5, TextPatternRangeEndpoint.Start, TextUnit.Character, -2, 0, 5 }; + yield return new object[] { 3, 6, TextPatternRangeEndpoint.Start, TextUnit.Character, -2, 1, 6 }; + yield return new object[] { 3, 6, TextPatternRangeEndpoint.End, TextUnit.Character, 1, 3, 7 }; + yield return new object[] { 3, 6, TextPatternRangeEndpoint.End, TextUnit.Character, -1, 3, 5 }; + yield return new object[] { 1, 2, TextPatternRangeEndpoint.Start, TextUnit.Word, 1, 4, 4 }; + yield return new object[] { 1, 2, TextPatternRangeEndpoint.Start, TextUnit.Word, 5, 11, 11 }; + yield return new object[] { 12, 14, TextPatternRangeEndpoint.Start, TextUnit.Word, -1, 11, 14 }; + yield return new object[] { 12, 14, TextPatternRangeEndpoint.Start, TextUnit.Word, -2, 8, 14 }; + } + + [StaTheory] + [MemberData(nameof(UiaTextRange_ITextRangeProvider_MoveEndpointByUnit_MovesCorrectly_TestData))] + internal void UiaTextRange_ITextRangeProvider_MoveEndpointByUnit_MovesCorrectly(int start, int end, TextPatternRangeEndpoint endpoint, TextUnit unit, int count, int expectedStart, int expectedEnd) + { + string testText = +@"This is the text to move on - line 1 +This is the line 2 +This is the line 3"; + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + Mock providerMock = new Mock(MockBehavior.Strict); + providerMock.Setup(m => m.Text).Returns(testText); + providerMock.Setup(m => m.TextLength).Returns(testText.Length); + UiaTextProvider provider = providerMock.Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start, end); + ((ITextRangeProvider)textRange).MoveEndpointByUnit(endpoint, unit, count); + Assert.Equal(expectedStart, textRange.Start); + Assert.Equal(expectedEnd, textRange.End); + } + + public static IEnumerable UiaTextRange_ITextRangeProvider_MoveEndpointByRange_MovesCorrectly_TestData() + { + yield return new object[] { 0, 5, TextPatternRangeEndpoint.Start, 7, 10, TextPatternRangeEndpoint.Start, 7, 7 }; + yield return new object[] { 0, 5, TextPatternRangeEndpoint.Start, 7, 10, TextPatternRangeEndpoint.End, 10, 10 }; + yield return new object[] { 0, 5, TextPatternRangeEndpoint.End, 7, 10, TextPatternRangeEndpoint.Start, 0, 7 }; + yield return new object[] { 0, 5, TextPatternRangeEndpoint.End, 7, 10, TextPatternRangeEndpoint.End, 0, 10 }; + } + + [StaTheory] + [MemberData(nameof(UiaTextRange_ITextRangeProvider_MoveEndpointByRange_MovesCorrectly_TestData))] + internal void UiaTextRange_ITextRangeProvider_MoveEndpointByRange_MovesCorrectly(int start, int end, TextPatternRangeEndpoint endpoint, int targetRangeStart, int targetRangeEnd, TextPatternRangeEndpoint targetEndpoint, int expectedStart, int expectedEnd) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start, end); + UiaTextRange targetRange = new UiaTextRange(enclosingElement, provider, targetRangeStart, targetRangeEnd); + ((ITextRangeProvider)textRange).MoveEndpointByRange(endpoint, targetRange, targetEndpoint); + Assert.Equal(expectedStart, textRange.Start); + Assert.Equal(expectedEnd, textRange.End); + } + + [StaTheory] + [InlineData(0, 0)] + [InlineData(0, 10)] + [InlineData(5, 10)] + public void UiaTextRange_ITextRangeProvider_Select_ReturnsCorrectValue(int start, int end) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + Mock providerMock = new Mock(MockBehavior.Strict); + providerMock.Setup(m => m.SetSelection(start, end)); + UiaTextProvider provider = providerMock.Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start, end); + ((ITextRangeProvider)textRange).Select(); + providerMock.Verify(m => m.SetSelection(start, end), Times.Once()); + } + + [StaFact] + public void UiaTextRange_ITextRangeProvider_AddToSelection_DoesntThrowException() + { + // Check an app doesn't crash when calling AddToSelectio method. + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, 3, 7); + ((ITextRangeProvider)textRange).AddToSelection(); + } + + [StaFact] + public void UiaTextRange_ITextRangeProvider_RemoveFromSelection_DoesntThrowException() + { + // Check an app doesn't crash when calling RemoveFromSelection method. + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, 3, 7); + ((ITextRangeProvider)textRange).RemoveFromSelection(); + } + + public static IEnumerable UiaTextRange_ITextRangeProvider_ScrollIntoView_Multiline_CallsLineScrollCorrectly_TestData() + { + yield return new object[] { 30, 35, 30, 1, 30, 0, 1 }; + yield return new object[] { 60, 65, 60, 2, 60, 2, 0 }; + } + + [StaTheory] + [MemberData(nameof(UiaTextRange_ITextRangeProvider_ScrollIntoView_Multiline_CallsLineScrollCorrectly_TestData))] + public void UiaTextRange_ITextRangeProvider_ScrollIntoView_Multiline_CallsLineScrollCorrectly(int start, int end, int charIndex, int lineForCharIndex, int charactersHorizontal, int linesVertical, int firstVisibleLine) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + + var providerMock = new Mock(MockBehavior.Strict); + providerMock.Setup(p => p.IsMultiline).Returns(true); + providerMock.Setup(p => p.GetLineFromCharIndex(charIndex)).Returns(lineForCharIndex); + providerMock.Setup(p => p.LineScroll(charactersHorizontal, linesVertical)).Returns(true); + providerMock.Setup(p => p.FirstVisibleLine).Returns(firstVisibleLine); + + UiaTextProvider provider = providerMock.Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start, end); + ((ITextRangeProvider)textRange).ScrollIntoView(BOOL.TRUE); + providerMock.Verify(e => e.LineScroll(charactersHorizontal, linesVertical), Times.Once()); + } + + public static IEnumerable UiaTextRange_ITextRangeProvider_ScrollIntoView_SingleLine_ExecutesCorrectly_TestData() + { + yield return new object[] { 0, 30, true, false }; + yield return new object[] { 70, 85, true, false }; + } + + [StaTheory] + [MemberData(nameof(UiaTextRange_ITextRangeProvider_ScrollIntoView_SingleLine_ExecutesCorrectly_TestData))] + public void UiaTextRange_ITextRangeProvider_ScrollIntoView_SingleLine_ExecutesCorrectly(int start, int end, bool scrollable, bool readingRTL) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + int visibleStart = 40; + int visibleEnd = 60; + + var providerMock = new Mock(MockBehavior.Strict); + providerMock.Setup(p => p.IsMultiline).Returns(false); + providerMock.Setup(p => p.IsScrollable).Returns(scrollable); + providerMock.Setup(p => p.IsReadingRTL).Returns(readingRTL); + providerMock.Setup(p => p.GetVisibleRangePoints(out visibleStart, out visibleEnd)); + + UiaTextProvider provider = providerMock.Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start, end); + ((ITextRangeProvider)textRange).ScrollIntoView(BOOL.TRUE); + providerMock.Verify(p => p.GetVisibleRangePoints(out visibleStart, out visibleEnd), Times.Exactly(2)); + } + + [StaFact] + public void UiaTextRange_ITextRangeProvider_GetChildren_ReturnsCorrectValue() + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start: 0, end: 0); + IRawElementProviderSimple[] actual = ((ITextRangeProvider)textRange).GetChildren(); + Assert.Empty(actual); + } + + [StaTheory] + [InlineData("", 0, true)] + [InlineData("", 5, true)] + [InlineData("", -5, true)] + [InlineData("Some text", 0, true)] + [InlineData("Some text", 5, false)] + [InlineData("Some text", 6, false)] + [InlineData("Some text", 99, true)] + [InlineData("Some text", -5, true)] + [InlineData("Some, text", 4, false)] + [InlineData("Some text", 4, false)] + [InlineData("1dsf'21gj", 3, false)] + [InlineData("1dsf'21gj", 4, false)] + [InlineData("1dsf'21gj", 6, false)] + [InlineData("1d??sf'21gj", 6, false)] + public void UiaTextRange_private_AtParagraphBoundary_ReturnsCorrectValue(string text, int index, bool expected) + { + bool actual = StaticNullTextRange.TestAccessor().AtParagraphBoundary(text, index); + Assert.Equal(expected, actual); + } + + [StaTheory] + [InlineData("", 0, true)] + [InlineData("", 5, true)] + [InlineData("", -5, true)] + [InlineData("Some text", 0, true)] + [InlineData("Some text", 5, true)] + [InlineData("Some text", 6, false)] + [InlineData("Some text", 99, true)] + [InlineData("Some text", -5, true)] + [InlineData("Some, text", 4, true)] + [InlineData("Some text", 4, true)] + [InlineData("1dsf'21gj", 3, false)] + [InlineData("1dsf'21gj", 4, false)] + [InlineData("1dsf'21gj", 6, false)] + [InlineData("1d??sf'21gj", 6, false)] + public void UiaTextRange_private_AtWordBoundary_ReturnsCorrectValue(string text, int index, bool expected) + { + bool actual = StaticNullTextRange.TestAccessor().AtWordBoundary(text, index); + Assert.Equal(expected, actual); + } + + [StaTheory] + [InlineData('\'', true)] + [InlineData((char)0x2019, true)] + [InlineData('\t', false)] + [InlineData('t', false)] + public void UiaTextRange_private_IsApostrophe_ReturnsCorrectValue(char ch, bool expected) + { + bool actual = StaticNullTextRange.TestAccessor().IsApostrophe(ch); + Assert.Equal(expected, actual); + } + + [StaTheory] + [InlineData((int)ES.CENTER, (int)HorizontalTextAlignment.Centered)] + [InlineData((int)ES.LEFT, (int)HorizontalTextAlignment.Left)] + [InlineData((int)ES.RIGHT, (int)HorizontalTextAlignment.Right)] + public void UiaTextRange_private_GetHorizontalTextAlignment_ReturnsCorrectValue(int style, int expected) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, 0, 0); + + HorizontalTextAlignment actual = textRange.TestAccessor().GetHorizontalTextAlignment((ES)style); + + Assert.Equal((HorizontalTextAlignment)expected, actual); + } + + [StaTheory] + [InlineData((int)(ES.UPPERCASE | ES.LEFT | ES.MULTILINE | ES.READONLY | ES.AUTOHSCROLL), (int)CapStyle.AllCap)] + [InlineData((int)(ES.LOWERCASE | ES.LEFT | ES.MULTILINE | ES.READONLY | ES.AUTOHSCROLL), (int)CapStyle.None)] + public void UiaTextRange_private_GetCapStyle_ReturnsExpectedValue(int editStyle, int expected) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, 0, 0); + + CapStyle actual = textRange.TestAccessor().GetCapStyle((ES)editStyle); + + Assert.Equal((CapStyle)expected, actual); + } + + [StaTheory] + [InlineData(true)] + [InlineData(false)] + public void UiaTextRange_private_GetReadOnly_ReturnsCorrectValue(bool readOnly) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + Mock providerMock = new Mock(MockBehavior.Strict); + providerMock.Setup(m => m.IsReadOnly).Returns(readOnly); + UiaTextProvider provider = providerMock.Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, 0, 0); + + bool actual = textRange.TestAccessor().GetReadOnly(); + + Assert.Equal(readOnly, actual); + } + + [StaFact] + public void UiaTextRange_private_GetBackgroundColor_ReturnsExpectedValue() + { + COLORREF actual = StaticNullTextRange.TestAccessor().GetBackgroundColor(); + uint expected = 0x00ffffff; // WINDOW system color + Assert.Equal(expected, actual); + } + + [StaTheory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("Some test text")] + public void UiaTextRange_private_GetFontName_ReturnsExpectedValue(string faceName) + { + LOGFONTW logfont = new LOGFONTW + { + FaceName = faceName + }; + + string actual = StaticNullTextRange.TestAccessor().GetFontName(logfont); + + Assert.Equal(faceName ?? "", actual); + } + + [StaTheory] + [InlineData(1, 1)] + [InlineData(5, 5)] + [InlineData(5.3, 5)] + [InlineData(9.5, 10)] + [InlineData(18, 18)] + [InlineData(18.8, 19)] + [InlineData(100, 100)] + public void UiaTextRange_private_GetFontSize_ReturnsCorrectValue(float fontSize, double expected) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + Mock providerMock = new Mock(MockBehavior.Strict); + using Font font = new Font("Arial", fontSize, FontStyle.Regular); + providerMock.Setup(m => m.Logfont).Returns(LOGFONTW.FromFont(font)); + UiaTextProvider provider = providerMock.Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, 5, 20); + + double actual = textRange.TestAccessor().GetFontSize(provider.Logfont); + + Assert.Equal(expected, actual); + } + + [StaTheory] + [InlineData(FW.BLACK)] + [InlineData(FW.BOLD)] + [InlineData(FW.DEMIBOLD)] + [InlineData(FW.DONTCARE)] + [InlineData(FW.EXTRABOLD)] + [InlineData(FW.EXTRALIGHT)] + [InlineData(FW.LIGHT)] + [InlineData(FW.MEDIUM)] + [InlineData(FW.NORMAL)] + [InlineData(FW.THIN)] + public void UiaTextRange_private_GetFontWeight_ReturnsCorrectValue(object fontWeight) + { + LOGFONTW logfont = new LOGFONTW() { lfWeight = (FW)fontWeight }; + FW actual = StaticNullTextRange.TestAccessor().GetFontWeight(logfont); + Assert.Equal(fontWeight, actual); + } + + [StaFact] + public void UiaTextRange_private_GetForegroundColor_ReturnsCorrectValue() + { + COLORREF actual = StaticNullTextRange.TestAccessor().GetForegroundColor(); + Assert.Equal(new COLORREF(), actual); + } + + [StaTheory] + [InlineData(0, false)] + [InlineData(5, true)] + public void UiaTextRange_private_GetItalic_ReturnsCorrectValue(byte ifItalic, bool expected) + { + LOGFONTW logfont = new LOGFONTW() { lfItalic = ifItalic }; + + bool actual = StaticNullTextRange.TestAccessor().GetItalic(logfont); + + Assert.Equal(expected, actual); + } + + [StaTheory] + [InlineData(0, (int)TextDecorationLineStyle.None)] + [InlineData(5, (int)TextDecorationLineStyle.Single)] + public void UiaTextRange_private_GetStrikethroughStyle_ReturnsCorrectValue(byte ifStrikeOut, int expected) + { + LOGFONTW logfont = new LOGFONTW() { lfStrikeOut = ifStrikeOut }; + TextDecorationLineStyle actual = StaticNullTextRange.TestAccessor().GetStrikethroughStyle(logfont); + + Assert.Equal((TextDecorationLineStyle)expected, actual); + } + + [StaTheory] + [InlineData(0, (int)TextDecorationLineStyle.None)] + [InlineData(5, (int)TextDecorationLineStyle.Single)] + public void UiaTextRange_private_GetUnderlineStyle_ReturnsCorrectValue(byte ifUnderline, int expected) + { + LOGFONTW logfont = new LOGFONTW() { lfUnderline = ifUnderline }; + TextDecorationLineStyle actual = StaticNullTextRange.TestAccessor().GetUnderlineStyle(logfont); + + Assert.Equal((TextDecorationLineStyle)expected, actual); + } + + [StaTheory] + [InlineData(0, 0)] + [InlineData(0, 10)] + [InlineData(5, 10)] + [InlineData(5, 100)] + [InlineData(100, 100)] + [InlineData(100, 200)] + public void UiaTextRange_private_MoveTo_SetValuesCorrectly(int start, int end) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, 0, 0); + + textRange.TestAccessor().MoveTo(start, end); + + Assert.Equal(start, textRange.Start); + Assert.Equal(end, textRange.End); + } + + [StaTheory] + [InlineData(-5, 0)] + [InlineData(0, -5)] + [InlineData(-10, -10)] + [InlineData(10, 5)] + public void UiaTextRange_private_MoveTo_ThrowsException_IfIncorrectParameters(int start, int end) + { + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + UiaTextProvider provider = new Mock(MockBehavior.Strict).Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, 0, 0); + + Assert.ThrowsAny(() => textRange.TestAccessor().MoveTo(start, end)); + } + + [StaTheory] + [InlineData(0, 0, 0, 0)] + [InlineData(0, 10, 0, 10)] + [InlineData(5, 10, 5, 10)] + [InlineData(5, 100, 5, 24)] + [InlineData(100, 100, 24, 24)] + [InlineData(100, 200, 24, 24)] + public void UiaTextRange_private_ValidateEndpoints_SetValuesCorrectly(int start, int end, int expectedStart, int expectedEnd) + { + string testText = "Some long long test text"; + IRawElementProviderSimple enclosingElement = new Mock(MockBehavior.Strict).Object; + Mock providerMock = new Mock(MockBehavior.Strict); + providerMock.Setup(m => m.TextLength).Returns(testText.Length); + UiaTextProvider provider = providerMock.Object; + UiaTextRange textRange = new UiaTextRange(enclosingElement, provider, start, end); + + textRange.TestAccessor().ValidateEndpoints(); + + Assert.Equal(expectedStart, textRange.Start); + Assert.Equal(expectedEnd, textRange.End); + } + } +} diff --git a/src/System.Windows.Forms/src/Properties/TypeForwards.cs b/src/System.Windows.Forms/src/Properties/TypeForwards.cs index f2be132b2ed..43a72c032f6 100644 --- a/src/System.Windows.Forms/src/Properties/TypeForwards.cs +++ b/src/System.Windows.Forms/src/Properties/TypeForwards.cs @@ -17,6 +17,7 @@ [assembly: TypeForwardedTo(typeof(System.Windows.Forms.Automation.AutomationLiveSetting))] [assembly: TypeForwardedTo(typeof(System.Windows.Forms.Automation.AutomationNotificationKind))] [assembly: TypeForwardedTo(typeof(System.Windows.Forms.Automation.AutomationNotificationProcessing))] +[assembly: TypeForwardedTo(typeof(System.Windows.Forms.Automation.IAutomationLiveRegion))] [assembly: TypeForwardedTo(typeof(System.Windows.Forms.VisualStyles.TextMetrics))] [assembly: TypeForwardedTo(typeof(System.Windows.Forms.VisualStyles.TextMetricsCharacterSet))] diff --git a/src/System.Windows.Forms/src/PublicAPI.Shipped.txt b/src/System.Windows.Forms/src/PublicAPI.Shipped.txt index 7f3c5a6228f..713e94cda3e 100644 --- a/src/System.Windows.Forms/src/PublicAPI.Shipped.txt +++ b/src/System.Windows.Forms/src/PublicAPI.Shipped.txt @@ -276,9 +276,6 @@ System.Windows.Forms.AutoValidate.Disable = 0 -> System.Windows.Forms.AutoValida System.Windows.Forms.AutoValidate.EnableAllowFocusChange = 2 -> System.Windows.Forms.AutoValidate System.Windows.Forms.AutoValidate.EnablePreventFocusChange = 1 -> System.Windows.Forms.AutoValidate System.Windows.Forms.AutoValidate.Inherit = -1 -> System.Windows.Forms.AutoValidate -System.Windows.Forms.Automation.IAutomationLiveRegion -System.Windows.Forms.Automation.IAutomationLiveRegion.LiveSetting.get -> System.Windows.Forms.Automation.AutomationLiveSetting -System.Windows.Forms.Automation.IAutomationLiveRegion.LiveSetting.set -> void System.Windows.Forms.AxHost System.Windows.Forms.AxHost.AboutBoxDelegate System.Windows.Forms.AxHost.ActiveXInvokeKind diff --git a/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt b/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt index e69de29bb2d..400a5b031fe 100644 --- a/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt +++ b/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt @@ -0,0 +1,3 @@ +~override System.Windows.Forms.RichTextBox.CreateAccessibilityInstance() -> System.Windows.Forms.AccessibleObject +~override System.Windows.Forms.TextBox.OnKeyUp(System.Windows.Forms.KeyEventArgs e) -> void +~override System.Windows.Forms.TextBox.OnMouseDown(System.Windows.Forms.MouseEventArgs e) -> void \ No newline at end of file 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 d28725f95ad..3bd8536bd84 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/AccessibleObject.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/AccessibleObject.cs @@ -43,7 +43,9 @@ public partial class AccessibleObject : UiaCore.ISelectionItemProvider, UiaCore.IRawElementProviderHwndOverride, UiaCore.IScrollItemProvider, - UiaCore.IMultipleViewProvider + UiaCore.IMultipleViewProvider, + UiaCore.ITextProvider, + UiaCore.ITextProvider2 { /// /// Specifies the interface used by this . @@ -64,6 +66,9 @@ public partial class AccessibleObject : // Indicates this object is being used ONLY to wrap a system IAccessible private readonly bool systemWrapper; + private UiaTextProvider? _textProvider; + private UiaTextProvider2? _textProvider2; + // The support for the UIA Notification event begins in RS3. // Assume the UIA Notification event is available until we learn otherwise. // If we learn that the UIA Notification event is not available, @@ -350,15 +355,13 @@ internal virtual int ProviderOptions internal virtual UiaCore.IRawElementProviderSimple? HostRawElementProvider => null; - internal virtual object? GetPropertyValue(UiaCore.UIA propertyID) - { - if (propertyID == UiaCore.UIA.IsInvokePatternAvailablePropertyId) + internal virtual object? GetPropertyValue(UiaCore.UIA propertyID) => + propertyID switch { - return IsInvokePatternAvailable; - } - - return null; - } + UiaCore.UIA.IsInvokePatternAvailablePropertyId => IsInvokePatternAvailable, + UiaCore.UIA.BoundingRectanglePropertyId => Bounds, + _ => null + }; private bool IsInvokePatternAvailable { @@ -474,6 +477,29 @@ internal virtual void Toggle() internal virtual void Invoke() => DoDefaultAction(); + internal virtual UiaCore.ITextRangeProvider? DocumentRangeInternal => _textProvider?.DocumentRange; + + internal virtual UiaCore.ITextRangeProvider[]? GetTextSelection() => _textProvider?.GetSelection(); + + internal virtual UiaCore.ITextRangeProvider[]? GetTextVisibleRanges() => _textProvider?.GetVisibleRanges(); + + internal virtual UiaCore.ITextRangeProvider? GetTextRangeFromChild(UiaCore.IRawElementProviderSimple childElement) + => _textProvider?.RangeFromChild(childElement); + + internal virtual UiaCore.ITextRangeProvider? GetTextRangeFromPoint(Point screenLocation) => _textProvider?.RangeFromPoint(screenLocation); + + internal virtual UiaCore.SupportedTextSelection SupportedTextSelectionInternal + => _textProvider?.SupportedTextSelection ?? UiaCore.SupportedTextSelection.None; + + internal virtual UiaCore.ITextRangeProvider? GetTextCaretRange(out BOOL isActive) + { + isActive = BOOL.FALSE; + return _textProvider2?.GetCaretRange(out isActive); + } + + internal virtual UiaCore.ITextRangeProvider? GetRangeFromAnnotation(UiaCore.IRawElementProviderSimple annotationElement) => + _textProvider2?.RangeFromAnnotation(annotationElement); + internal virtual bool IsReadOnly => false; internal virtual void SetValue(string? newValue) @@ -673,6 +699,37 @@ UiaCore.IRawElementProviderSimple[] UiaCore.ILegacyIAccessibleProvider.GetSelect void UiaCore.IInvokeProvider.Invoke() => Invoke(); + UiaCore.ITextRangeProvider? UiaCore.ITextProvider.DocumentRange => DocumentRangeInternal; + + UiaCore.ITextRangeProvider[]? UiaCore.ITextProvider.GetSelection() => GetTextSelection(); + + UiaCore.ITextRangeProvider[]? UiaCore.ITextProvider.GetVisibleRanges() => GetTextVisibleRanges(); + + UiaCore.ITextRangeProvider? UiaCore.ITextProvider.RangeFromChild(UiaCore.IRawElementProviderSimple childElement) => + GetTextRangeFromChild(childElement); + + UiaCore.ITextRangeProvider? UiaCore.ITextProvider.RangeFromPoint(Point screenLocation) => GetTextRangeFromPoint(screenLocation); + + UiaCore.SupportedTextSelection UiaCore.ITextProvider.SupportedTextSelection => SupportedTextSelectionInternal; + + UiaCore.ITextRangeProvider? UiaCore.ITextProvider2.DocumentRange => DocumentRangeInternal; + + UiaCore.ITextRangeProvider[]? UiaCore.ITextProvider2.GetSelection() => GetTextSelection(); + + UiaCore.ITextRangeProvider[]? UiaCore.ITextProvider2.GetVisibleRanges() => GetTextVisibleRanges(); + + UiaCore.ITextRangeProvider? UiaCore.ITextProvider2.RangeFromChild(UiaCore.IRawElementProviderSimple childElement) => + GetTextRangeFromChild(childElement); + + UiaCore.ITextRangeProvider? UiaCore.ITextProvider2.RangeFromPoint(Point screenLocation) => GetTextRangeFromPoint(screenLocation); + + UiaCore.SupportedTextSelection UiaCore.ITextProvider2.SupportedTextSelection => SupportedTextSelectionInternal; + + UiaCore.ITextRangeProvider? UiaCore.ITextProvider2.GetCaretRange(out BOOL isActive) => GetTextCaretRange(out isActive); + + UiaCore.ITextRangeProvider? UiaCore.ITextProvider2.RangeFromAnnotation(UiaCore.IRawElementProviderSimple annotationElement) => + GetRangeFromAnnotation(annotationElement); + BOOL UiaCore.IValueProvider.IsReadOnly => IsReadOnly ? BOOL.TRUE : BOOL.FALSE; string? UiaCore.IValueProvider.Value => Value; @@ -1595,6 +1652,12 @@ protected void UseStdAccessibleObjects(IntPtr handle, int objid) } } + internal void UseTextProviders(UiaTextProvider textProvider, UiaTextProvider2 textProvider2) + { + _textProvider = textProvider ?? throw new ArgumentNullException(nameof(textProvider)); + _textProvider2 = textProvider2 ?? throw new ArgumentNullException(nameof(textProvider2)); + } + /// /// Performs custom navigation between parent/child/sibling accessible /// objects. This is basically just a wrapper for GetSysChild(), that 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 dedf92aba1b..97ea7e55874 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/InternalAccessibleObject.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/InternalAccessibleObject.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics.CodeAnalysis; +using System.Drawing; using System.Globalization; using System.Reflection; using System.Runtime.InteropServices; @@ -40,7 +41,9 @@ internal sealed class InternalAccessibleObject : ISelectionItemProvider, IScrollItemProvider, IRawElementProviderHwndOverride, - IMultipleViewProvider + IMultipleViewProvider, + ITextProvider, + ITextProvider2 { private IAccessible publicIAccessible; // AccessibleObject as IAccessible private readonly Oleaut32.IEnumVariant publicIEnumVariant; // AccessibleObject as Oleaut32.IEnumVariant @@ -69,6 +72,8 @@ internal sealed class InternalAccessibleObject : private readonly IScrollItemProvider publicIScrollItemProvider; // AccessibleObject as IScrollItemProvider private readonly IRawElementProviderHwndOverride publicIRawElementProviderHwndOverride; // AccessibleObject as IRawElementProviderHwndOverride private readonly IMultipleViewProvider publicIMultiViewProvider; // AccessibleObject as IMultipleViewProvider + private readonly ITextProvider publicITextProvider; // AccessibleObject as ITextProvider + private readonly ITextProvider2 publicITextProvider2; // AccessibleObject as ITextProvider2 /// /// Create a new wrapper. @@ -100,6 +105,8 @@ internal InternalAccessibleObject(AccessibleObject accessibleImplemention) publicIScrollItemProvider = (IScrollItemProvider)accessibleImplemention; publicIRawElementProviderHwndOverride = (IRawElementProviderHwndOverride)accessibleImplemention; publicIMultiViewProvider = (IMultipleViewProvider)accessibleImplemention; + publicITextProvider = (ITextProvider)accessibleImplemention; + publicITextProvider2 = (ITextProvider2)accessibleImplemention; // Note: Deliberately not holding onto AccessibleObject to enforce all access through the interfaces } @@ -314,6 +321,8 @@ ProviderOptions IRawElementProviderSimple.ProviderOptions UIA.SelectionItemPatternId => (ISelectionItemProvider)this, UIA.ScrollItemPatternId => (IScrollItemProvider)this, UIA.MultipleViewPatternId => (IMultipleViewProvider)this, + UIA.TextPatternId => (ITextProvider)this, + UIA.TextPattern2Id => (ITextProvider2)this, _ => null }; } @@ -365,8 +374,7 @@ UiaRect IRawElementProviderFragment.BoundingRectangle void ILegacyIAccessibleProvider.DoDefaultAction() => publicILegacyIAccessibleProvider.DoDefaultAction(); - IAccessible? ILegacyIAccessibleProvider.GetIAccessible() - => publicILegacyIAccessibleProvider.GetIAccessible(); + IAccessible? ILegacyIAccessibleProvider.GetIAccessible() => publicILegacyIAccessibleProvider.GetIAccessible(); IRawElementProviderSimple[] ILegacyIAccessibleProvider.GetSelection() => publicILegacyIAccessibleProvider.GetSelection(); @@ -376,6 +384,40 @@ UiaRect IRawElementProviderFragment.BoundingRectangle void IInvokeProvider.Invoke() => publicIInvokeProvider.Invoke(); + ITextRangeProvider[]? ITextProvider.GetSelection() => publicITextProvider.GetSelection(); + + ITextRangeProvider[]? ITextProvider.GetVisibleRanges() => publicITextProvider.GetVisibleRanges(); + + ITextRangeProvider? ITextProvider.RangeFromChild(IRawElementProviderSimple childElement) + => publicITextProvider.RangeFromChild(childElement); + + ITextRangeProvider? ITextProvider.RangeFromPoint(Point screenLocation) + => publicITextProvider.RangeFromPoint(screenLocation); + + SupportedTextSelection ITextProvider.SupportedTextSelection => publicITextProvider.SupportedTextSelection; + + ITextRangeProvider? ITextProvider.DocumentRange => publicITextProvider.DocumentRange; + + ITextRangeProvider[]? ITextProvider2.GetSelection() => publicITextProvider2.GetSelection(); + + ITextRangeProvider[]? ITextProvider2.GetVisibleRanges() => publicITextProvider2.GetVisibleRanges(); + + ITextRangeProvider? ITextProvider2.RangeFromChild(IRawElementProviderSimple childElement) + => publicITextProvider2.RangeFromChild(childElement); + + ITextRangeProvider? ITextProvider2.RangeFromPoint(Point screenLocation) + => publicITextProvider2.RangeFromPoint(screenLocation); + + SupportedTextSelection ITextProvider2.SupportedTextSelection => publicITextProvider2.SupportedTextSelection; + + ITextRangeProvider? ITextProvider2.DocumentRange => publicITextProvider2.DocumentRange; + + ITextRangeProvider? ITextProvider2.GetCaretRange(out BOOL isActive) + => publicITextProvider2.GetCaretRange(out isActive); + + ITextRangeProvider? ITextProvider2.RangeFromAnnotation(IRawElementProviderSimple annotationElement) + => publicITextProvider2.RangeFromAnnotation(annotationElement); + BOOL IValueProvider.IsReadOnly => publicIValueProvider.IsReadOnly; string? IValueProvider.Value => publicIValueProvider.Value; diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/RichTextBox.cs b/src/System.Windows.Forms/src/System/Windows/Forms/RichTextBox.cs index 8706f2ee944..a76031139ba 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/RichTextBox.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/RichTextBox.cs @@ -2257,7 +2257,7 @@ private RichTextBoxSelectionAttribute GetCharFormat(CFM mask, CFE effect) return charFormat; } - Font GetCharFormatFont(bool selectionOnly) + private Font GetCharFormatFont(bool selectionOnly) { ForceHandleCreate(); @@ -3237,6 +3237,8 @@ private void UserPreferenceChangedHandler(object o, UserPreferenceChangedEventAr } } + protected override AccessibleObject CreateAccessibilityInstance() => new ControlAccessibleObject(this); + /// /// Creates the IRichEditOleCallback compatible object for handling RichEdit callbacks. For more /// information look up the MSDN info on this interface. This is designed to be a back door of diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/TextBox.cs b/src/System.Windows.Forms/src/System/Windows/Forms/TextBox.cs index a2b02a0c540..433b16d8bb0 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/TextBox.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/TextBox.cs @@ -647,6 +647,48 @@ protected override void OnHandleDestroyed(EventArgs e) base.OnHandleDestroyed(e); } + protected override void OnKeyUp(KeyEventArgs e) + { + base.OnKeyUp(e); + + if (IsHandleCreated && ContainsNavigationKeyCode(e.KeyCode)) + { + AccessibilityObject?.RaiseAutomationEvent(UiaCore.UIA.Text_TextSelectionChangedEventId); + } + } + + private bool ContainsNavigationKeyCode(Keys keyCode) + { + switch (keyCode) + { + case Keys.Up: + case Keys.Down: + case Keys.PageUp: + case Keys.PageDown: + case Keys.Home: + case Keys.End: + case Keys.Left: + case Keys.Right: + return true; + default: + return false; + } + } + + protected override void OnMouseDown(MouseEventArgs e) + { + base.OnMouseDown(e); + + if (IsHandleCreated) + { + // As there is no corresponding windows notification + // about text selection changed for TextBox assuming + // that any mouse down on textbox leads to change of + // the caret position and thereby change the selection. + AccessibilityObject?.RaiseAutomationEvent(UiaCore.UIA.Text_TextSelectionChangedEventId); + } + } + protected virtual void OnTextAlignChanged(EventArgs e) { if (Events[EVENT_TEXTALIGNCHANGED] is EventHandler eh) diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/TextBoxBase.TextBoxBaseAccessibleObject.cs b/src/System.Windows.Forms/src/System/Windows/Forms/TextBoxBase.TextBoxBaseAccessibleObject.cs new file mode 100644 index 00000000000..dc40f5b967c --- /dev/null +++ b/src/System.Windows.Forms/src/System/Windows/Forms/TextBoxBase.TextBoxBaseAccessibleObject.cs @@ -0,0 +1,46 @@ +// 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; + +namespace System.Windows.Forms +{ + public abstract partial class TextBoxBase + { + internal class TextBoxBaseAccessibleObject : ControlAccessibleObject + { + private readonly TextBoxBase _owningTextBoxBase; + private readonly TextBoxBaseUiaTextProvider _textProvider; + + public TextBoxBaseAccessibleObject(TextBoxBase owner) : base(owner) + { + _owningTextBoxBase = owner; + _textProvider = new TextBoxBaseUiaTextProvider(owner); + + UseTextProviders(_textProvider, _textProvider); + } + + internal override bool IsIAccessibleExSupported() => true; + + internal override bool IsPatternSupported(UiaCore.UIA patternId) => + patternId switch + { + UiaCore.UIA.TextPatternId => true, + UiaCore.UIA.TextPattern2Id => true, + _ => base.IsPatternSupported(patternId) + }; + + internal override object? GetPropertyValue(UiaCore.UIA propertyID) => + propertyID switch + { + UiaCore.UIA.IsTextPatternAvailablePropertyId => IsPatternSupported(UiaCore.UIA.TextPatternId), + UiaCore.UIA.IsTextPattern2AvailablePropertyId => IsPatternSupported(UiaCore.UIA.TextPattern2Id), + UiaCore.UIA.IsValuePatternAvailablePropertyId => IsPatternSupported(UiaCore.UIA.ValuePatternId), + _ => base.GetPropertyValue(propertyID) + }; + + internal override bool IsReadOnly => _owningTextBoxBase.ReadOnly; + } + } +} diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/TextBoxBase.TextBoxBaseUiaTextProvider.cs b/src/System.Windows.Forms/src/System/Windows/Forms/TextBoxBase.TextBoxBaseUiaTextProvider.cs new file mode 100644 index 00000000000..6deaf4b1061 --- /dev/null +++ b/src/System.Windows.Forms/src/System/Windows/Forms/TextBoxBase.TextBoxBaseUiaTextProvider.cs @@ -0,0 +1,381 @@ +// 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.Runtime.InteropServices; +using System.Windows.Forms.Automation; +using static Interop; +using static Interop.User32; + +namespace System.Windows.Forms +{ + public abstract partial class TextBoxBase + { + internal class TextBoxBaseUiaTextProvider : UiaTextProvider2 + { + private readonly TextBoxBase _owningTextBoxBase; + + public TextBoxBaseUiaTextProvider(TextBoxBase owner) + { + _owningTextBoxBase = owner ?? throw new ArgumentNullException(nameof(owner)); + } + + public override UiaCore.ITextRangeProvider[]? GetSelection() + { + if (!_owningTextBoxBase.IsHandleCreated) + { + return null; + } + + // First caret position of a selected text + int start = 0; + // Last caret position of a selected text + int end = 0; + + // Returns info about a selected text range. + // If there is no selection, start and end parameters are the position of the caret. + SendMessageW(_owningTextBoxBase, (WM)EM.GETSEL, ref start, ref end); + + var internalAccessibleObject = new InternalAccessibleObject(_owningTextBoxBase.AccessibilityObject); + return new UiaCore.ITextRangeProvider[] { new UiaTextRange(internalAccessibleObject, this, start, end) }; + } + + public override UiaCore.ITextRangeProvider[]? GetVisibleRanges() + { + if (!_owningTextBoxBase.IsHandleCreated) + { + return null; + } + + GetVisibleRangePoints(out int start, out int end); + var internalAccessibleObject = new InternalAccessibleObject(_owningTextBoxBase.AccessibilityObject); + + return new UiaCore.ITextRangeProvider[] { new UiaTextRange(internalAccessibleObject, this, start, end) }; + } + + public override UiaCore.ITextRangeProvider? RangeFromChild(UiaCore.IRawElementProviderSimple childElement) + { + // We don't have any children so this call returns null. + Debug.Fail("Text edit control cannot have a child element."); + return null; + } + + /// + /// Returns the degenerate (empty) text range nearest to the specified screen coordinates. + /// + /// The location in screen coordinates. + /// A degenerate range nearest the specified location. Null is never returned. + public override UiaCore.ITextRangeProvider? RangeFromPoint(Point screenLocation) + { + if (!_owningTextBoxBase.IsHandleCreated) + { + return null; + } + + Point clientLocation = screenLocation; + + // Convert screen to client coordinates. + // (Essentially ScreenToClient but MapWindowPoints accounts for window mirroring using WS_EX_LAYOUTRTL.) + if (MapWindowPoints(new HandleRef(null, IntPtr.Zero), new HandleRef(this, _owningTextBoxBase.Handle), ref clientLocation, 1) == 0) + { + return new UiaTextRange(new InternalAccessibleObject(_owningTextBoxBase.AccessibilityObject), this, 0, 0); + } + + // We have to deal with the possibility that the coordinate is inside the window rect + // but outside the client rect. In that case we just scoot it over so it is at the nearest + // point in the client rect. + RECT clientRectangle = _owningTextBoxBase.ClientRectangle; + + clientLocation.X = Math.Max(clientLocation.X, clientRectangle.left); + clientLocation.X = Math.Min(clientLocation.X, clientRectangle.right); + clientLocation.Y = Math.Max(clientLocation.Y, clientRectangle.top); + clientLocation.Y = Math.Min(clientLocation.Y, clientRectangle.bottom); + + // Get the character at those client coordinates. + int start = _owningTextBoxBase.GetCharIndexFromPosition(clientLocation); + + return new UiaTextRange(new InternalAccessibleObject(_owningTextBoxBase.AccessibilityObject), this, start, start); + } + + public override UiaCore.ITextRangeProvider DocumentRange => new UiaTextRange(new InternalAccessibleObject(_owningTextBoxBase.AccessibilityObject), this, 0, TextLength); + + public override UiaCore.SupportedTextSelection SupportedTextSelection => UiaCore.SupportedTextSelection.Single; + + public override UiaCore.ITextRangeProvider? GetCaretRange(out BOOL isActive) + { + isActive = BOOL.FALSE; + + if (!_owningTextBoxBase.IsHandleCreated) + { + return null; + } + + var hasKeyboardFocus = _owningTextBoxBase.AccessibilityObject.GetPropertyValue(UiaCore.UIA.HasKeyboardFocusPropertyId); + if (hasKeyboardFocus is bool && (bool)hasKeyboardFocus) + { + isActive = BOOL.TRUE; + } + + var internalAccessibleObject = new InternalAccessibleObject(_owningTextBoxBase.AccessibilityObject); + + return new UiaTextRange(internalAccessibleObject, this, _owningTextBoxBase.SelectionStart, _owningTextBoxBase.SelectionStart); + } + + public override Point PointToScreen(Point pt) => _owningTextBoxBase.PointToScreen(pt); + + /// + /// Exposes a text range that contains the text that is the target of the annotation associated with the specified annotation element. + /// + /// + /// The provider for an element that implements the IAnnotationProvider interface. + /// The annotation element is a sibling of the element that implements the interface for the document. + /// + /// + /// A text range that contains the annotation target text. + /// + public override UiaCore.ITextRangeProvider RangeFromAnnotation(UiaCore.IRawElementProviderSimple annotationElement) + { + var internalAccessibleObject = new InternalAccessibleObject(_owningTextBoxBase.AccessibilityObject); + + return new UiaTextRange(internalAccessibleObject, this, 0, 0); + } + + public override Rectangle BoundingRectangle + => _owningTextBoxBase.IsHandleCreated + ? (Rectangle)GetFormattingRectangle() + : Rectangle.Empty; + + public override int FirstVisibleLine + => _owningTextBoxBase.IsHandleCreated + ? (int)(long)SendMessageW(_owningTextBoxBase, (WM)EM.GETFIRSTVISIBLELINE) + : -1; + + public override bool IsMultiline => _owningTextBoxBase.Multiline; + + public override bool IsReadingRTL + => _owningTextBoxBase.IsHandleCreated + ? WindowExStyle.HasFlag(WS_EX.RTLREADING) + : false; + + public override bool IsReadOnly => _owningTextBoxBase.ReadOnly; + + public override bool IsScrollable + { + get + { + if (!_owningTextBoxBase.IsHandleCreated) + { + return false; + } + + ES extendedStyle = (ES)(long)GetWindowLong(_owningTextBoxBase, GWL.STYLE); + return extendedStyle.HasFlag(ES.AUTOHSCROLL) || extendedStyle.HasFlag(ES.AUTOVSCROLL); + } + } + + public override int LinesCount + => _owningTextBoxBase.IsHandleCreated + ? (int)(long)SendMessageW(new HandleRef(this, _owningTextBoxBase.Handle), (WM)EM.GETLINECOUNT) + : -1; + + public override int LinesPerPage + { + get + { + if (!_owningTextBoxBase.IsHandleCreated) + { + return -1; + } + + Rectangle rect = _owningTextBoxBase.ClientRectangle; + if (rect.IsEmpty) + { + return 0; + } + + if (!_owningTextBoxBase.Multiline) + { + return 1; + } + + int fontHeight = _owningTextBoxBase.Font.Height; + return fontHeight != 0 ? (int)Math.Ceiling(((double)rect.Height) / fontHeight) : 0; + } + } + + public override LOGFONTW Logfont + => _owningTextBoxBase.IsHandleCreated + ? LOGFONTW.FromFont(_owningTextBoxBase.Font) + : default; + + public override string Text + => _owningTextBoxBase.IsHandleCreated + ? _owningTextBoxBase.Text + : string.Empty; + + public override int TextLength + => _owningTextBoxBase.IsHandleCreated + ? (int)(long)SendMessageW(_owningTextBoxBase, WM.GETTEXTLENGTH) + : -1; + + public override WS_EX WindowExStyle + => _owningTextBoxBase.IsHandleCreated + ? GetWindowExStyle(_owningTextBoxBase.Handle) + : WS_EX.LEFT; + + public override WS WindowStyle + => _owningTextBoxBase.IsHandleCreated + ? GetWindowStyle(_owningTextBoxBase.Handle) + : WS.OVERLAPPED; + + public override ES EditStyle + => _owningTextBoxBase.IsHandleCreated + ? GetEditStyle(_owningTextBoxBase.Handle) + : ES.LEFT; + + public override int GetLineFromCharIndex(int charIndex) + => _owningTextBoxBase.IsHandleCreated + ? _owningTextBoxBase.GetLineFromCharIndex(charIndex) + : -1; + + public override int GetLineIndex(int line) + => _owningTextBoxBase.IsHandleCreated + ? (int)(long)SendMessageW(_owningTextBoxBase, (WM)EM.LINEINDEX, (IntPtr)line) + : -1; + + public override Point GetPositionFromChar(int charIndex) + => _owningTextBoxBase.IsHandleCreated + ? _owningTextBoxBase.GetPositionFromCharIndex(charIndex) + : Point.Empty; + + // A variation on EM_POSFROMCHAR that returns the upper-right corner instead of upper-left. + public override Point GetPositionFromCharForUpperRightCorner(int startCharIndex, string text) + { + if (!_owningTextBoxBase.IsHandleCreated || startCharIndex < 0 || startCharIndex >= text.Length) + { + return Point.Empty; + } + + char ch = text[startCharIndex]; + Point pt; + + if (char.IsControl(ch)) + { + if (ch == '\t') + { + // for tabs the calculated width of the character is no help so we use the + // UL corner of the following character if it is on the same line. + bool useNext = startCharIndex < TextLength - 1 && GetLineFromCharIndex(startCharIndex + 1) == GetLineFromCharIndex(startCharIndex); + return _owningTextBoxBase.GetPositionFromCharIndex(useNext ? startCharIndex + 1 : startCharIndex); + } + + pt = _owningTextBoxBase.GetPositionFromCharIndex(startCharIndex); + + if (ch == '\r' || ch == '\n') + { + pt.X += EndOfLineWidth; // add 2 px to show the end of line + } + + // return the UL corner of the rest characters because these characters have no width + return pt; + } + + // get the UL corner of the character + pt = _owningTextBoxBase.GetPositionFromCharIndex(startCharIndex); + + // add the width of the character at that position. + if (GetTextExtentPoint32(ch, out Size size)) + { + pt.X += size.Width; + } + + return pt; + } + + public override void GetVisibleRangePoints(out int visibleStart, out int visibleEnd) + { + visibleStart = 0; + visibleEnd = 0; + + if (!_owningTextBoxBase.IsHandleCreated || IsDegenerate(_owningTextBoxBase.ClientRectangle)) + { + return; + } + + Rectangle rectangle = GetFormattingRectangle(); + if (IsDegenerate(rectangle)) + { + return; + } + + // Formatting rectangle is the boundary, which we need to inflate by 1 + // in order to read characters within the rectangle + Point ptStart = new Point(rectangle.X + 1, rectangle.Y + 1); + Point ptEnd = new Point(rectangle.Right - 1, rectangle.Bottom - 1); + + visibleStart = _owningTextBoxBase.GetCharIndexFromPosition(ptStart); + visibleEnd = _owningTextBoxBase.GetCharIndexFromPosition(ptEnd) + 1; // Add 1 to get a caret position after received character + + return; + + bool IsDegenerate(Rectangle rect) + => rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0; + } + + public override bool LineScroll(int charactersHorizontal, int linesVertical) + // Sends an EM_LINESCROLL message to scroll it horizontally and/or vertically. + => _owningTextBoxBase.IsHandleCreated + && SendMessageW(_owningTextBoxBase, (WM)EM.LINESCROLL, (IntPtr)charactersHorizontal, (IntPtr)linesVertical) != IntPtr.Zero; + + public override void SetSelection(int start, int end) + { + if (!_owningTextBoxBase.IsHandleCreated) + { + return; + } + + if (start < 0 || start > TextLength) + { + Debug.Fail("SetSelection start is out of text range."); + return; + } + + if (end < 0 || end > TextLength) + { + Debug.Fail("SetSelection end is out of text range."); + return; + } + + SendMessageW(_owningTextBoxBase, (WM)EM.SETSEL, (IntPtr)start, (IntPtr)end); + } + + private RECT GetFormattingRectangle() + { + Debug.Assert(_owningTextBoxBase.IsHandleCreated); + + // Send an EM_GETRECT message to find out the bounding rectangle. + RECT rectangle = new RECT(); + SendMessageW(_owningTextBoxBase, (WM)EM.GETRECT, (IntPtr)0, ref rectangle); + return rectangle; + } + + private bool GetTextExtentPoint32(char item, out Size size) + { + Debug.Assert(_owningTextBoxBase.IsHandleCreated); + + size = new Size(); + + using var hdc = new GetDcScope(_owningTextBoxBase.Handle); + if (hdc.IsNull) + { + return false; + } + + // Add the width of the character at that position. + return Gdi32.GetTextExtentPoint32W(hdc, item.ToString(), 1, ref size).IsTrue(); + } + } + } +} diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/TextBoxBase.cs b/src/System.Windows.Forms/src/System/Windows/Forms/TextBoxBase.cs index 315cbf7654b..0fdddba4e5e 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/TextBoxBase.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/TextBoxBase.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -25,7 +25,7 @@ namespace System.Windows.Forms [DefaultEvent(nameof(TextChanged))] [DefaultBindingProperty(nameof(Text))] [Designer("System.Windows.Forms.Design.TextBoxBaseDesigner, " + AssemblyRef.SystemDesign)] - public abstract class TextBoxBase : Control + public abstract partial class TextBoxBase : Control { // The boolean properties for this control are contained in the textBoxFlags bit // vector. We can store up to 32 boolean values in this one vector. Here we @@ -1124,6 +1124,7 @@ public int SelectionStart { throw new ArgumentOutOfRangeException(nameof(value), value, string.Format(SR.InvalidArgument, nameof(SelectionStart), value)); } + Select(value, SelectionLength); } } @@ -1364,6 +1365,8 @@ public void ClearUndo() /// public void Copy() => SendMessageW(this, WM.COPY); + protected override AccessibleObject CreateAccessibilityInstance() => new TextBoxBaseAccessibleObject(this); + protected override void CreateHandle() { // This "creatingHandle" stuff is to avoid property change events @@ -1653,7 +1656,7 @@ public virtual int GetCharIndexFromPosition(Point pt) /// public virtual int GetLineFromCharIndex(int index) { - return unchecked((int)(long)SendMessageW(this, (WM)EM.LINEFROMCHAR, (IntPtr)index)); + return (int)(long)SendMessageW(this, (WM)EM.LINEFROMCHAR, (IntPtr)index); } /// @@ -1666,7 +1669,7 @@ public virtual Point GetPositionFromCharIndex(int index) return Point.Empty; } - int i = (int)User32.SendMessageW(this, (WM)EM.POSFROMCHAR, (IntPtr)index); + int i = (int)(long)SendMessageW(this, (WM)EM.POSFROMCHAR, (IntPtr)index); return new Point(PARAM.SignedLOWORD(i), PARAM.SignedHIWORD(i)); } @@ -1812,6 +1815,7 @@ public void Select(int start, int length) { length = (int)longLength; } + start = textLen; } @@ -1834,6 +1838,8 @@ private protected virtual void SelectInternal(int start, int length, int textLen AdjustSelectionStartAndEnd(start, length, out int s, out int e, textLen); SendMessageW(this, (WM)EM.SETSEL, (IntPtr)s, (IntPtr)e); + + AccessibilityObject?.RaiseAutomationEvent(UiaCore.UIA.Text_TextSelectionChangedEventId); } else { diff --git a/src/System.Windows.Forms/tests/UnitTests/AccessibleObjects/TextBoxAccessibleObjectTests.cs b/src/System.Windows.Forms/tests/UnitTests/AccessibleObjects/TextBoxAccessibleObjectTests.cs new file mode 100644 index 00000000000..d9f0faf0c5e --- /dev/null +++ b/src/System.Windows.Forms/tests/UnitTests/AccessibleObjects/TextBoxAccessibleObjectTests.cs @@ -0,0 +1,37 @@ +// 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; + +namespace System.Windows.Forms.Tests.AccessibleObjects +{ + public class TextBoxAccessibleObjectTests + { + [WinFormsTheory] + [InlineData((int)Interop.UiaCore.UIA.IsTextPatternAvailablePropertyId)] + [InlineData((int)Interop.UiaCore.UIA.IsTextPattern2AvailablePropertyId)] + public void TextBoxAccessibleObject_TextPatternAvailable(int propertyId) + { + using TextBox textBox = new TextBox(); + AccessibleObject textBoxAccessibleObject = textBox.AccessibilityObject; + + // Interop.UiaCore.UIA accessible level (internal) is less than the test level (public) so it needs boxing and unboxing + Assert.True((bool)textBoxAccessibleObject.GetPropertyValue((Interop.UiaCore.UIA)propertyId)); + Assert.False(textBox.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData((int)Interop.UiaCore.UIA.TextPatternId)] + [InlineData((int)Interop.UiaCore.UIA.TextPattern2Id)] + public void TextBoxAccessibleObject_TextPatternSupported(int patternId) + { + using TextBox textBox = new TextBox(); + AccessibleObject textBoxAccessibleObject = textBox.AccessibilityObject; + + // Interop.UiaCore.UIA accessible level (internal) is less than the test level (public) so it needs boxing and unboxing + Assert.True(textBoxAccessibleObject.IsPatternSupported((Interop.UiaCore.UIA)patternId)); + Assert.False(textBox.IsHandleCreated); + } + } +} diff --git a/src/System.Windows.Forms/tests/UnitTests/AccessibleObjects/TextBoxBaseAccessibleObjectTests.cs b/src/System.Windows.Forms/tests/UnitTests/AccessibleObjects/TextBoxBaseAccessibleObjectTests.cs new file mode 100644 index 00000000000..1a286bb35e2 --- /dev/null +++ b/src/System.Windows.Forms/tests/UnitTests/AccessibleObjects/TextBoxBaseAccessibleObjectTests.cs @@ -0,0 +1,103 @@ +// 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 Xunit; + +namespace System.Windows.Forms.Tests.AccessibleObjects +{ + public class TextBoxBaseAccessibleObjectTests + { + [WinFormsFact] + public void TextBoxBaseAccessibleObject_ctor_default() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + + AccessibleObject textBoxAccessibleObject = textBoxBase.AccessibilityObject; + Assert.NotNull(textBoxAccessibleObject); + + TextBoxBase.TextBoxBaseAccessibleObject textBoxBaseAccessibleObject = new TextBoxBase.TextBoxBaseAccessibleObject(textBoxBase); + Assert.NotNull(textBoxBaseAccessibleObject); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData((int)Interop.UiaCore.UIA.IsTextPatternAvailablePropertyId)] + [InlineData((int)Interop.UiaCore.UIA.IsTextPattern2AvailablePropertyId)] + public void TextBoxBaseAccessibleObject_TextPatternAvailable(int propertyId) + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + AccessibleObject textBoxAccessibleObject = textBoxBase.AccessibilityObject; + + // Interop.UiaCore.UIA accessible level (internal) is less than the test level (public) so it needs boxing and unboxing + Assert.True((bool)textBoxAccessibleObject.GetPropertyValue((Interop.UiaCore.UIA)propertyId)); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData((int)Interop.UiaCore.UIA.TextPatternId)] + [InlineData((int)Interop.UiaCore.UIA.TextPattern2Id)] + public void TextBoxBaseAccessibleObject_TextPatternSupported(int patternId) + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + AccessibleObject textBoxAccessibleObject = textBoxBase.AccessibilityObject; + + // Interop.UiaCore.UIA accessible level (internal) is less than the test level (public) so it needs boxing and unboxing + Assert.True(textBoxAccessibleObject.IsPatternSupported((Interop.UiaCore.UIA)patternId)); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData(true)] + [InlineData(false)] + public void TextBoxBaseAccessibleObject_IsReadOnly_ReturnsCorrectValue(bool readOnly) + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + AccessibleObject accessibleObject = textBoxBase.AccessibilityObject; + + textBoxBase.ReadOnly = readOnly; + Assert.Equal(textBoxBase.ReadOnly, accessibleObject.IsReadOnly); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseAccessibleObject_Value_ReturnsEmpty_WithoutHandle() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + textBoxBase.Text = "Some test text"; + AccessibleObject accessibleObject = textBoxBase.AccessibilityObject; + Assert.Equal(string.Empty, accessibleObject.Value); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseAccessibleObject_Value_EqualsText() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + textBoxBase.CreateControl(); + textBoxBase.Text = "Some test text"; + AccessibleObject accessibleObject = textBoxBase.AccessibilityObject; + Assert.Equal(textBoxBase.Text, accessibleObject.Value); + Assert.True(textBoxBase.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData(50, 20)] + [InlineData(100, 10)] + public void TextBoxBaseAccessibleObject_BoundingRectangle_IsCorrect(int width, int height) + { + using TextBoxBase textBoxBase = new SubTextBoxBase { Size = new Size(width, height) }; + textBoxBase.CreateControl(); + AccessibleObject accessibleObject = textBoxBase.AccessibilityObject; + Rectangle expected = textBoxBase.RectangleToScreen(textBoxBase.ClientRectangle); // Forces Handle creating + Rectangle actual = accessibleObject.BoundingRectangle; + Assert.Equal(expected, actual); + Assert.True(textBoxBase.IsHandleCreated); + } + + private class SubTextBoxBase : TextBoxBase + { } + } +} diff --git a/src/System.Windows.Forms/tests/UnitTests/AccessibleObjects/UpDownEditAccessibleObjectTests.cs b/src/System.Windows.Forms/tests/UnitTests/AccessibleObjects/UpDownEditAccessibleObjectTests.cs new file mode 100644 index 00000000000..43d38c58ea0 --- /dev/null +++ b/src/System.Windows.Forms/tests/UnitTests/AccessibleObjects/UpDownEditAccessibleObjectTests.cs @@ -0,0 +1,30 @@ +// 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 Interop.UiaCore; + +namespace System.Windows.Forms.Tests.AccessibleObjects +{ + public class UpDownEditAccessibleObjectTests + { + [WinFormsFact] + public void UpDownEditAccessibleObject_ctor_default() + { + using UpDownBase upDown = new SubUpDownBase(); + using UpDownBase.UpDownEdit upDownEdit = new UpDownBase.UpDownEdit(upDown); + Assert.NotNull(upDownEdit.AccessibilityObject); + Assert.False(upDown.IsHandleCreated); + } + + private class SubUpDownBase : UpDownBase + { + public override void DownButton() { } + + public override void UpButton() { } + + protected override void UpdateEditText() { } + } + } +} diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/RichTextBoxTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/RichTextBoxTests.cs index b4b8565ca6c..be34407fc63 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/RichTextBoxTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/RichTextBoxTests.cs @@ -12,6 +12,7 @@ using System.Threading; using WinForms.Common.Tests; using Xunit; +using Xunit.Abstractions; using static Interop; using static Interop.Richedit; using static Interop.User32; @@ -1604,40 +1605,56 @@ public void RichTextBox_Font_SetWithNonNullOldValueWithTextWithHandle_GetReturns public static IEnumerable Font_GetCharFormat_TestData() { - yield return new object[] { new Font("Arial", 8.25f), 165, 0 }; - yield return new object[] { new Font("Arial", 8.25f, FontStyle.Bold), 165, CFE.BOLD }; - yield return new object[] { new Font("Arial", 8.25f, FontStyle.Italic), 165, CFE.ITALIC }; - yield return new object[] { new Font("Arial", 8.25f, FontStyle.Strikeout), 165, CFE.STRIKEOUT }; - yield return new object[] { new Font("Arial", 8.25f, FontStyle.Underline), 165, CFE.UNDERLINE }; - yield return new object[] { new Font("Arial", 8.25f, FontStyle.Bold | FontStyle.Italic | FontStyle.Regular | FontStyle.Strikeout | FontStyle.Underline, GraphicsUnit.Point, 10), 165, CFE.BOLD | CFE.ITALIC | CFE.UNDERLINE | CFE.STRIKEOUT }; + yield return new object[] { "Arial", 8.25f, FontStyle.Regular, GraphicsUnit.Point, 1, 165, 0 }; + yield return new object[] { "Arial", 8.25f, FontStyle.Bold, GraphicsUnit.Point, 1, 165, CFE.BOLD }; + yield return new object[] { "Arial", 8.25f, FontStyle.Italic, GraphicsUnit.Point, 1, 165, CFE.ITALIC }; + yield return new object[] { "Arial", 8.25f, FontStyle.Strikeout, GraphicsUnit.Point, 1, 165, CFE.STRIKEOUT }; + yield return new object[] { "Arial", 8.25f, FontStyle.Underline, GraphicsUnit.Point, 1, 165, CFE.UNDERLINE }; + yield return new object[] { "Arial", 8.25f, FontStyle.Bold | FontStyle.Italic | FontStyle.Regular | FontStyle.Strikeout | FontStyle.Underline, GraphicsUnit.Point, 10, 165, CFE.BOLD | CFE.ITALIC | CFE.UNDERLINE | CFE.STRIKEOUT }; } [WinFormsTheory] [MemberData(nameof(Font_GetCharFormat_TestData))] - public unsafe void RichTextBox_Font_GetCharFormat_Success(Font value, int expectedYHeight, int expectedEffects) + public unsafe void RichTextBox_Font_GetCharFormat_Success(string familyName, float emSize, FontStyle style, GraphicsUnit unit, byte gdiCharSet, int expectedYHeight, int expectedEffects) { using var control = new RichTextBox(); - Assert.NotEqual(IntPtr.Zero, control.Handle); - control.Font = value; + var format = new CHARFORMAT2W { cbSize = (uint)sizeof(CHARFORMAT2W), dwMask = (CFM)int.MaxValue }; - Assert.NotEqual(IntPtr.Zero, SendMessageW(control.Handle, (WM)Richedit.EM.GETCHARFORMAT, (IntPtr)SCF.ALL, ref format)); - Assert.Equal("Arial", format.FaceName.ToString()); - Assert.Equal(expectedYHeight, (int)format.yHeight); - Assert.Equal(CFE.AUTOBACKCOLOR | CFE.AUTOCOLOR | (CFE)expectedEffects, format.dwEffects); - Assert.Equal(0, format.bPitchAndFamily); - // Set null. - control.Font = null; - Assert.NotEqual(IntPtr.Zero, SendMessageW(control.Handle, (WM)Richedit.EM.GETCHARFORMAT, (IntPtr)SCF.ALL, ref format)); - Assert.Equal(Control.DefaultFont.Name, format.FaceName.ToString()); - Assert.Equal((int)(Control.DefaultFont.SizeInPoints * 20), (int)format.yHeight); - Assert.Equal(CFE.AUTOBACKCOLOR | CFE.AUTOCOLOR, format.dwEffects); - Assert.Equal(0, format.bPitchAndFamily); + IntPtr result; + + using (var font = new Font(familyName, emSize, style, unit, gdiCharSet)) + { + control.Font = font; + result = SendMessageW(control.Handle, (WM)Richedit.EM.GETCHARFORMAT, (IntPtr)SCF.ALL, ref format); + Assert.NotEqual(IntPtr.Zero, result); + Assert.Equal(familyName, format.FaceName.ToString()); + Assert.Equal(expectedYHeight, format.yHeight); + Assert.Equal(CFE.AUTOBACKCOLOR | CFE.AUTOCOLOR | (CFE)expectedEffects, format.dwEffects); + Assert.Equal(0, format.bPitchAndFamily); + + // Set null. + control.Font = null; + } + + var format1 = new CHARFORMAT2W + { + cbSize = (uint)sizeof(CHARFORMAT2W), + dwMask = (CFM)int.MaxValue + }; + + result = SendMessageW(control.Handle, (WM)Richedit.EM.GETCHARFORMAT, (IntPtr)SCF.ALL, ref format1); + Assert.NotEqual(IntPtr.Zero, result); + Assert.Equal(Control.DefaultFont.Name, format1.FaceName.ToString()); + Assert.Equal((int)(Control.DefaultFont.SizeInPoints * 20), (int)format1.yHeight); + Assert.True(format1.dwEffects.HasFlag(CFE.AUTOBACKCOLOR)); + Assert.True(format1.dwEffects.HasFlag(CFE.AUTOCOLOR)); + Assert.Equal(0, format1.bPitchAndFamily); } [WinFormsFact] @@ -4818,14 +4835,15 @@ public void RichTextBox_SelectionFont_GetDisposed_ThrowsObjectDisposedException( public static IEnumerable SelectionFont_Set_TestData() { - yield return new object[] { new Font("Arial", 8.25f), 1 }; - yield return new object[] { new Font("Arial", 8.25f, FontStyle.Bold | FontStyle.Italic | FontStyle.Regular | FontStyle.Strikeout | FontStyle.Underline, GraphicsUnit.Point, 10), 1 }; + yield return new object[] { "Arial", 8.25f, false, FontStyle.Regular, 0, 0, 1 }; + yield return new object[] { "Arial", 8.25f, true, FontStyle.Bold | FontStyle.Italic | FontStyle.Regular | FontStyle.Strikeout | FontStyle.Underline, GraphicsUnit.Point, 10, 1 }; } [WinFormsTheory] [MemberData(nameof(SelectionFont_Set_TestData))] - public void RichTextBox_SelectionFont_Set_GetReturnsExpected(Font value, byte expectedGdiCharset) + public void RichTextBox_SelectionFont_Set_GetReturnsExpected(string fontName, float fontSize, bool hasStyle, FontStyle fontStyle, GraphicsUnit units, byte gdiCharSet, byte expectedGdiCharset) { + using Font value = hasStyle ? new Font(fontName, fontSize, fontStyle, units, gdiCharSet) : new Font(fontName, fontSize); using var control = new RichTextBox { SelectionFont = value @@ -4850,8 +4868,9 @@ public void RichTextBox_SelectionFont_Set_GetReturnsExpected(Font value, byte ex [WinFormsTheory] [MemberData(nameof(SelectionFont_Set_TestData))] - public void RichTextBox_SelectionFont_SetWithHandle_GetReturnsExpected(Font value, byte expectedGdiCharset) + public void RichTextBox_SelectionFont_SetWithHandle_GetReturnsExpected(string fontName, float fontSize, bool hasStyle, FontStyle fontStyle, GraphicsUnit units, byte gdiCharSet, byte expectedGdiCharset) { + using Font value = hasStyle ? new Font(fontName, fontSize, fontStyle, units, gdiCharSet) : new Font(fontName, fontSize); using var control = new RichTextBox(); Assert.NotEqual(IntPtr.Zero, control.Handle); int invalidatedCallCount = 0; diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/TextBoxBase.TextBoxBaseUiaTextProviderTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/TextBoxBase.TextBoxBaseUiaTextProviderTests.cs new file mode 100644 index 00000000000..179bd142ef2 --- /dev/null +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/TextBoxBase.TextBoxBaseUiaTextProviderTests.cs @@ -0,0 +1,923 @@ +// 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 System.Windows.Forms.Automation; +using Xunit; +using Xunit.Abstractions; +using static System.Windows.Forms.TextBoxBase; +using static Interop; +using static Interop.Gdi32; +using static Interop.User32; + +namespace System.Windows.Forms.Tests +{ + public class TextBoxBase_TextBoxBaseUiaTextProviderTests : IClassFixture + { + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_ctor_DoesntCreateControlHandle() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + Assert.False(textBoxBase.IsHandleCreated); + + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_Ctor_ThrowsException_IfOwnerIsNull() + { + Assert.Throws(() => new TextBoxBaseUiaTextProvider(null)); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_IsMultiline_IsCorrect() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + textBoxBase.Multiline = false; + Assert.False(provider.IsMultiline); + + textBoxBase.Multiline = true; + Assert.True(provider.IsMultiline); + + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData(true)] + [InlineData(false)] + public void TextBoxBaseUiaTextProvider_IsReadOnly_IsCorrect(bool readOnly) + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + textBoxBase.ReadOnly = readOnly; + Assert.Equal(readOnly, provider.IsReadOnly); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_IsScrollable_IsCorrect() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + textBoxBase.CreateControl(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + Assert.True(provider.IsScrollable); + Assert.True(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_IsScrollable_False_WithoutHandle() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + Assert.False(provider.IsScrollable); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseTextProvider_GetWindowStyle_ReturnsNoneForNotInitializedControl() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + Assert.Equal(WS.OVERLAPPED, provider.WindowStyle); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData(RightToLeft.Yes, true)] + [InlineData(RightToLeft.No, false)] + public void TextBoxBaseUiaTextProvider_IsReadingRTL_ReturnsCorrectValue(RightToLeft rightToLeft, bool expectedResult) + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + textBoxBase.CreateControl(); + textBoxBase.RightToLeft = rightToLeft; + + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + Assert.Equal(expectedResult, provider.IsReadingRTL); + Assert.True(textBoxBase.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData(RightToLeft.Yes)] + [InlineData(RightToLeft.No)] + public void TextBoxBaseUiaTextProvider_IsReadingRTL_ReturnsFalse_WithoutHandle(RightToLeft rightToLeft) + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + textBoxBase.RightToLeft = rightToLeft; + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + Assert.False(provider.IsReadingRTL); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_DocumentRange_IsNotNull() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + Assert.NotNull(provider.DocumentRange); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_SupportedTextSelection_IsNotNull() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + UiaCore.SupportedTextSelection uiaTextRange = provider.SupportedTextSelection; + Assert.Equal(UiaCore.SupportedTextSelection.Single, uiaTextRange); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_GetCaretRange_IsNotNull() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + textBoxBase.CreateControl(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + UiaCore.ITextRangeProvider uiaTextRange = provider.GetCaretRange(out _); + Assert.NotNull(uiaTextRange); + Assert.True(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_GetCaretRange_IsNull_IfHandleIsNotCreated() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + UiaCore.ITextRangeProvider uiaTextRange = provider.GetCaretRange(out _); + Assert.Null(uiaTextRange); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData(30, 0, 0)] + [InlineData(30, 19, 1)] // Only 1 lines are placed at a height equal to 19 + [InlineData(30, 50, 3)] // Only 3 lines are placed at a height equal to 50 + [InlineData(30, 100, 6)] // Only 6 lines are placed at a height equal to 100 + public void TextBoxBaseUiaTextProvider_LinesPerPage_IsCorrect_with_handle(int width, int height, int lines) + { + using TextBoxBase textBoxBase = new SubTextBoxBase { Size = new Size(width, height) }; + textBoxBase.CreateControl(); + Assert.True(textBoxBase.IsHandleCreated); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + Assert.Equal(1, provider.LinesPerPage); + + textBoxBase.Multiline = true; + Assert.Equal(lines, provider.LinesPerPage); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_LinesPerPage_ReturnsMinusOne_WithoutHandle() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + Assert.Equal(-1, provider.LinesPerPage); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_FirstVisibleLine_Get_ReturnsCorrectValue() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + textBoxBase.Multiline = true; + textBoxBase.Size = new Size(50, 100); + textBoxBase.CreateControl(); + Assert.True(textBoxBase.IsHandleCreated); + + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + Assert.Equal(0, provider.FirstVisibleLine); + + provider.LineScroll(0, 2); + Assert.Equal(0, provider.FirstVisibleLine); + + textBoxBase.Text = "Some long long test text for testing GetFirstVisibleLine method"; + provider.LineScroll(0, 2); + Assert.Equal(2, provider.FirstVisibleLine); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_FirstVisibleLine_Get_ReturnsMinuOne_WithoutHandle() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + int line = provider.FirstVisibleLine; + Assert.Equal(-1, line); + + textBoxBase.Multiline = true; + textBoxBase.Size = new Size(30, 100); + + line = provider.FirstVisibleLine; + Assert.Equal(-1, line); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_LineCount_Get_ReturnsCorrectValue() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + textBoxBase.CreateControl(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + Assert.Equal(1, provider.LinesCount); + + textBoxBase.Multiline = true; + textBoxBase.Size = new Size(30, 50); + Assert.Equal(1, provider.LinesCount); + + textBoxBase.Text += "1\r\n"; + Assert.Equal(2, provider.LinesCount); + + textBoxBase.Text += "2\r\n"; + Assert.Equal(3, provider.LinesCount); + + textBoxBase.Text += "3\r\n"; + Assert.Equal(4, provider.LinesCount); + + textBoxBase.Text += "4\r\n"; + Assert.Equal(5, provider.LinesCount); + + Assert.True(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_LineCount_Get_ReturnsMinusOne_WithoutHandle() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + Assert.Equal(-1, provider.LinesCount); + + textBoxBase.Multiline = true; + textBoxBase.Size = new Size(30, 50); + Assert.Equal(-1, provider.LinesCount); + + textBoxBase.Text += "1\r\n"; + Assert.Equal(-1, provider.LinesCount); + + Assert.False(textBoxBase.IsHandleCreated); + } + + public static IEnumerable TextBoxBase_GetLineFromCharIndex_TestData() + { + yield return new object[] { new Size(50, 20), false, 0, 0 }; + yield return new object[] { new Size(50, 20), false, 50, 0 }; + yield return new object[] { new Size(100, 50), true, 50, 3 }; + yield return new object[] { new Size(50, 50), true, 50, 8 }; + } + + [WinFormsTheory] + [MemberData(nameof(TextBoxBase_GetLineFromCharIndex_TestData))] + public void TextBoxBaseUiaTextProvider_GetLineFromCharIndex_ReturnsCorrectValue(Size size, bool multiline, int charIndex, int expectedLine) + { + using SubTextBoxBase textBoxBase = new SubTextBoxBase() { Size = size, Multiline = multiline }; + textBoxBase.CreateControl(); + textBoxBase.Text = "Some test text for testing GetLineFromCharIndex method"; + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + int actualLine = provider.GetLineFromCharIndex(charIndex); + Assert.Equal(expectedLine, actualLine); + Assert.True(textBoxBase.IsHandleCreated); + } + +#pragma warning disable xUnit1026 // Disable xUnit1026 warning: The method doesn't use parameter 'expectedLine' + [WinFormsTheory] + [MemberData(nameof(TextBoxBase_GetLineFromCharIndex_TestData))] + public void TextBoxBaseUiaTextProvider_GetLineFromCharIndex_ReturnsMinusOne_WithoutHandle(Size size, bool multiline, int charIndex, int expectedLine) + { + using SubTextBoxBase textBoxBase = new SubTextBoxBase() + { + Size = size, + Multiline = multiline, + Text = "Some test text for testing GetLineFromCharIndex method" + }; + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + int actualLine = provider.GetLineFromCharIndex(charIndex); + + Assert.Equal(-1, actualLine); + Assert.False(textBoxBase.IsHandleCreated); + } +#pragma warning restore xUnit1026 + + public static IEnumerable TextBoxBaseUiaTextProvider_GetLineIndex_TestData() + { + yield return new object[] { new Size(50, 20), false, 0, 0 }; + yield return new object[] { new Size(50, 20), false, 3, 0 }; + yield return new object[] { new Size(50, 50), true, 3, 19 }; + yield return new object[] { new Size(100, 50), true, 3, 40 }; + yield return new object[] { new Size(50, 50), true, 100, -1 }; + } + + [WinFormsTheory] + [MemberData(nameof(TextBoxBaseUiaTextProvider_GetLineIndex_TestData))] + public void TextBoxBaseUiaTextProvider_GetLineIndex_ReturnsCorrectValue(Size size, bool multiline, int lineIndex, int expectedIndex) + { + using SubTextBoxBase textBoxBase = new SubTextBoxBase() + { + Size = size, + Multiline = multiline, + Text = "Some test text for testing GetLineIndex method" + }; + textBoxBase.CreateControl(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + int actualIndex = provider.GetLineIndex(lineIndex); + + Assert.Equal(expectedIndex, actualIndex); + Assert.True(textBoxBase.IsHandleCreated); + } + +#pragma warning disable xUnit1026 // Disable xUnit1026 warning: The method doesn't use parameter 'expectedIndex' + [WinFormsTheory] + [MemberData(nameof(TextBoxBaseUiaTextProvider_GetLineIndex_TestData))] + public void TextBoxBaseUiaTextProvider_GetLineIndex_ReturnsMinusOne_WithoutHandle(Size size, bool multiline, int lineIndex, int expectedIndex) + { + using SubTextBoxBase textBoxBase = new SubTextBoxBase() + { + Size = size, + Multiline = multiline, + Text = "Some test text for testing GetLineIndex method" + }; + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + int actualIndex = provider.GetLineIndex(lineIndex); + + Assert.Equal(-1, actualIndex); + Assert.False(textBoxBase.IsHandleCreated); + } +#pragma warning restore xUnit1026 + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_GetLogfont_ReturnsCorrectValue() + { + using (new NoAssertContext()) + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + textBoxBase.CreateControl(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + LOGFONTW expected = LOGFONTW.FromFont(textBoxBase.Font); + LOGFONTW actual = provider.Logfont; + Assert.False(string.IsNullOrEmpty(actual.FaceName.ToString())); + Assert.Equal(expected, actual); + Assert.True(textBoxBase.IsHandleCreated); + } + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_GetLogfont_ReturnsEmpty_WithoutHandle() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + LOGFONTW expected = new LOGFONTW(); + LOGFONTW actual = provider.Logfont; + Assert.True(string.IsNullOrEmpty(actual.FaceName.ToString())); + Assert.Equal(expected, actual); + Assert.False(textBoxBase.IsHandleCreated); + } + + public static IEnumerable TextBoxBaseUiaTextProvider_GetPositionFromChar_TestData() + { + yield return new object[] { new Size(50, 20), "Some test text for testing", false, 0, new Point(1, 0) }; + yield return new object[] { new Size(50, 20), "Some test text for testing", false, 15, new Point(79, 0) }; + yield return new object[] { new Size(50, 20), "Some test text for testing", true, 15, new Point(27, 30) }; + yield return new object[] { new Size(100, 60), "This is a\r\nlong long text\r\nfor testing\r\nGetPositionFromChar method", true, 0, new Point(4, 1) }; + yield return new object[] { new Size(100, 60), "This is a\r\nlong long text\r\nfor testing\r\nGetPositionFromChar method", true, 6, new Point(31, 1) }; + yield return new object[] { new Size(100, 60), "This is a\r\nlong long text\r\nfor testing\r\nGetPositionFromChar method", true, 26, new Point(78, 16) }; + } + + [WinFormsTheory] + [MemberData(nameof(TextBoxBaseUiaTextProvider_GetPositionFromChar_TestData))] + public void TextBoxBaseUiaTextProvider_GetPositionFromChar_ReturnsCorrectValue(Size size, string text, bool multiline, int charIndex, Point expectedPoint) + { + using SubTextBoxBase textBoxBase = new SubTextBoxBase() { Size = size, Text = text, Multiline = multiline }; + textBoxBase.CreateControl(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + Point actualPoint = provider.GetPositionFromChar(charIndex); + + Assert.True(actualPoint.X >= expectedPoint.X - 1 || actualPoint.X <= expectedPoint.X + 1); + Assert.True(actualPoint.Y >= expectedPoint.Y - 1 || actualPoint.Y <= expectedPoint.Y + 1); + Assert.True(textBoxBase.IsHandleCreated); + } + +#pragma warning disable xUnit1026 // Disable xUnit1026 warning: The method doesn't use parameter 'expectedPoint' + [WinFormsTheory] + [MemberData(nameof(TextBoxBaseUiaTextProvider_GetPositionFromChar_TestData))] + public void TextBoxBaseUiaTextProvider_GetPositionFromChar_ReturnsEmpty_WithoutHanlde(Size size, string text, bool multiline, int charIndex, Point expectedPoint) + { + using SubTextBoxBase textBoxBase = new SubTextBoxBase() { Size = size, Text = text, Multiline = multiline }; + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + Point actualPoint = provider.GetPositionFromChar(charIndex); + Assert.Equal(Point.Empty, actualPoint); + Assert.False(textBoxBase.IsHandleCreated); + } +#pragma warning restore xUnit1026 + + public static IEnumerable TextBoxBaseUiaTextProvider_GetPositionFromCharForUpperRightCorner_ReturnsCorrectValue_TestData() + { + yield return new object[] { new Size(50, 20), "", false, 0, new Point(0, 0) }; + yield return new object[] { new Size(50, 20), "Some test text", false, 100, new Point(0, 0) }; + yield return new object[] { new Size(50, 20), "Some test text", false, -1, new Point(0, 0) }; + yield return new object[] { new Size(50, 20), "Some test text", false, 12, new Point(71, 0) }; + yield return new object[] { new Size(50, 20), "Some test text", true, 12, new Point(19, 30) }; + yield return new object[] { new Size(100, 60), "Some test \n text", false, 10, new Point(56, 0) }; + yield return new object[] { new Size(100, 60), "Some test \n text", true, 10, new Point(59, 1) }; + yield return new object[] { new Size(100, 60), "Some test \r\n text", false, 10, new Point(56, 0) }; + yield return new object[] { new Size(100, 60), "Some test \r\n text", true, 10, new Point(59, 1) }; + yield return new object[] { new Size(100, 60), "Some test \r\n text", false, 12, new Point(60, 0) }; + yield return new object[] { new Size(100, 60), "Some test \r\n text", true, 12, new Point(7, 16) }; + yield return new object[] { new Size(100, 60), "Some test \t text", false, 10, new Point(57, 0) }; + yield return new object[] { new Size(100, 60), "Some test \t text", true, 10, new Point(60, 1) }; + yield return new object[] { new Size(40, 60), "Some test \t text", true, 12, new Point(8, 46) }; + } + + [WinFormsTheory] + [MemberData(nameof(TextBoxBaseUiaTextProvider_GetPositionFromCharForUpperRightCorner_ReturnsCorrectValue_TestData))] + public void TextBoxBaseUiaTextProvider_GetPositionFromCharForUpperRightCorner_ReturnsCorrectValue(Size size, string text, bool multiline, int charIndex, Point expectedPoint) + { + using SubTextBoxBase textBoxBase = new SubTextBoxBase() { Size = size, Text = text, Multiline = multiline }; + textBoxBase.CreateControl(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + Point actualPoint = provider.GetPositionFromCharForUpperRightCorner(charIndex, textBoxBase.Text); + Assert.True(actualPoint.X >= expectedPoint.X - 1 || actualPoint.X <= expectedPoint.X + 1); + Assert.True(actualPoint.Y >= expectedPoint.Y - 1 || actualPoint.Y <= expectedPoint.Y + 1); + Assert.True(textBoxBase.IsHandleCreated); + } + +#pragma warning disable xUnit1026 // Disable xUnit1026 warning: The method doesn't use parameter 'expectedPoint' + [WinFormsTheory] + [MemberData(nameof(TextBoxBaseUiaTextProvider_GetPositionFromCharForUpperRightCorner_ReturnsCorrectValue_TestData))] + public void TextBoxBaseUiaTextProvider_GetPositionFromCharForUpperRightCorner_ReturnsMinusOne_WithoutHandle(Size size, string text, bool multiline, int charIndex, Point expectedPoint) + { + using SubTextBoxBase textBoxBase = new SubTextBoxBase() { Size = size, Text = text, Multiline = multiline }; + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + Point actualPoint = provider.GetPositionFromCharForUpperRightCorner(charIndex, textBoxBase.Text); + Assert.Equal(Point.Empty, actualPoint); + Assert.False(textBoxBase.IsHandleCreated); + } +#pragma warning restore xUnit1026 + + public static IEnumerable TextBoxBaseUiaTextProvider_GetFormattingRectangle_TestData() + { + yield return new object[] { false, new Size(0, 0), new Rectangle(1, 0, 78, 16) }; + yield return new object[] { false, new Size(50, 50), new Rectangle(1, 1, 44, 15) }; + yield return new object[] { false, new Size(250, 100), new Rectangle(1, 1, 244, 15) }; + yield return new object[] { true, new Size(0, 0), new Rectangle(4, 0, 72, 16) }; + yield return new object[] { true, new Size(50, 50), new Rectangle(4, 1, 38, 30) }; + yield return new object[] { true, new Size(250, 100), new Rectangle(4, 1, 238, 90) }; + } + + [WinFormsTheory] + [MemberData(nameof(TextBoxBaseUiaTextProvider_GetFormattingRectangle_TestData))] + public void TextBoxBaseUiaTextProvider_GetFormattingRectangle_ReturnsCorrectValue(bool multiline, Size size, Rectangle expectedRectangle) + { + using SubTextBoxBase textBoxBase = new SubTextBoxBase() { Size = size, Multiline = multiline }; + textBoxBase.CreateControl(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + Rectangle providerRectangle = provider.BoundingRectangle; + + Assert.Equal(expectedRectangle, providerRectangle); + Assert.True(textBoxBase.IsHandleCreated); + } + +#pragma warning disable xUnit1026 // Disable xUnit1026 warning: The method doesn't use parameter 'expectedRectangle' + [WinFormsTheory] + [MemberData(nameof(TextBoxBaseUiaTextProvider_GetFormattingRectangle_TestData))] + public void TextBoxBaseUiaTextProvider_GetFormattingRectangle_ReturnsEmpty_WithoutHandle(bool multiline, Size size, Rectangle expectedRectangle) + { + using SubTextBoxBase textBoxBase = new SubTextBoxBase() { Size = size, Multiline = multiline }; + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + Rectangle providerRectangle = provider.BoundingRectangle; + + Assert.Equal(Drawing.Rectangle.Empty, providerRectangle); + Assert.False(textBoxBase.IsHandleCreated); + } +#pragma warning restore xUnit1026 + + [WinFormsTheory] + [InlineData("")] + [InlineData("Text")] + [InlineData("Some test text")] + public void TextBoxBaseUiaTextProvider_Text_ReturnsCorrectValue(string text) + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + textBoxBase.Text = text; + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + textBoxBase.CreateControl(); + string expected = textBoxBase.Text; + string actual = provider.Text.Trim('\0'); + Assert.Equal(expected, actual); + Assert.True(textBoxBase.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData("")] + [InlineData("Text")] + [InlineData("Some test text")] + public void TextBoxBaseUiaTextProvider_Text_ReturnsEmpty_WithoutHandle(string text) + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + textBoxBase.Text = text; + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + string expected = string.Empty; + string actual = provider.Text.Trim('\0'); + Assert.Equal(expected, actual); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData("")] + [InlineData("Text")] + [InlineData("Some test text for testing")] + [InlineData("Some very very very long test text for testing GetTextLength method")] + public void TextBoxBaseUiaTextProvider_TextLength_ReturnsCorrectValue(string text) + { + using TextBoxBase textBoxBase = new SubTextBoxBase + { + Text = text + }; + textBoxBase.CreateControl(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + Assert.Equal(textBoxBase.Text.Length, provider.TextLength); + Assert.True(textBoxBase.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData("")] + [InlineData("Text")] + [InlineData("Some test text")] + public void TextBoxBaseUiaTextProvider_TextLength_ReturnsMinusOne_WithoutHandle(string text) + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + textBoxBase.Text = text; + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + Assert.Equal(-1, provider.TextLength); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_WindowExStyle_ReturnsCorrectValue() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + textBoxBase.CreateControl(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + WS_EX actual = provider.WindowExStyle; + Assert.Equal(WS_EX.CLIENTEDGE, actual); + Assert.True(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_WindowExStyle_ReturnsLeft_WithoutHandle() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + WS_EX actual = provider.WindowExStyle; + Assert.Equal(WS_EX.LEFT, actual); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_EditStyle_ReturnsCorrectValue() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + textBoxBase.CreateControl(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + ES actual = provider.EditStyle; + Assert.True(actual.HasFlag(ES.LEFT)); + Assert.True(actual.HasFlag(ES.AUTOVSCROLL)); + Assert.True(actual.HasFlag(ES.AUTOHSCROLL)); + Assert.True(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_EditStyle_ReturnsLeft_WithoutHandle() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + ES actual = provider.EditStyle; + Assert.Equal(ES.LEFT, actual); + Assert.False(textBoxBase.IsHandleCreated); + } + + public static IEnumerable TextBoxBase_GetVisibleRangePoints_ForSinglelineTextBox_TestData() + { + yield return new object[] { new Size(0, 0), 0, 0 }; + yield return new object[] { new Size(0, 20), 0, 0 }; + yield return new object[] { new Size(30, 30), 0, 4 }; + yield return new object[] { new Size(50, 20), 0, 8 }; + yield return new object[] { new Size(150, 20), 0, 26 }; + } + + [WinFormsTheory] + [MemberData(nameof(TextBoxBase_GetVisibleRangePoints_ForSinglelineTextBox_TestData))] + public void TextBoxBaseUiaTextProvider_GetVisibleRangePoints_ForSinglelineTextBox_ReturnsCorrectValue(Size size, int expectedStart, int expectedEnd) + { + using SubTextBoxBase textBoxBase = new SubTextBoxBase() + { + Multiline = false, + Text = "Some test text for testing", + Size = size + }; + textBoxBase.CreateControl(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + provider.GetVisibleRangePoints(out int providerVisibleStart, out int providerVisibleEnd); + + Assert.True(providerVisibleStart >= 0); + Assert.True(providerVisibleStart < textBoxBase.Text.Length); + Assert.True(providerVisibleEnd >= 0); + Assert.True(providerVisibleEnd <= textBoxBase.Text.Length); + + Assert.Equal(expectedStart, providerVisibleStart); + Assert.Equal(expectedEnd, providerVisibleEnd); + Assert.True(textBoxBase.IsHandleCreated); + } + + public static IEnumerable TextBoxBaseUiaTextProvider_GetVisibleRangePoints_ForMultilineTextBox_TestData() + { + yield return new object[] { new Size(0, 0), 0, 0 }; + yield return new object[] { new Size(0, 20), 0, 0 }; + yield return new object[] { new Size(30, 30), 0, 3 }; + yield return new object[] { new Size(50, 20), 0, 6 }; + yield return new object[] { new Size(120, 20), 0, 20 }; + yield return new object[] { new Size(50, 80), 0, 26 }; + } + + [WinFormsTheory] + [MemberData(nameof(TextBoxBaseUiaTextProvider_GetVisibleRangePoints_ForMultilineTextBox_TestData))] + public void TextBoxBaseUiaTextProvider_GetVisibleRangePoints_ForMultilineTextBox_ReturnsCorrectValue(Size size, int expectedStart, int expectedEnd) + { + using SubTextBoxBase textBoxBase = new SubTextBoxBase() + { + Multiline = true, + Text = "Some test text for testing", + Size = size, + WordWrap = true + }; + textBoxBase.CreateControl(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + provider.GetVisibleRangePoints(out int providerVisibleStart, out int providerVisibleEnd); + + Assert.True(providerVisibleStart >= 0); + Assert.True(providerVisibleStart < textBoxBase.Text.Length); + Assert.True(providerVisibleEnd >= 0); + Assert.True(providerVisibleEnd <= textBoxBase.Text.Length); + + Assert.Equal(expectedStart, providerVisibleStart); + Assert.Equal(expectedEnd, providerVisibleEnd); + Assert.True(textBoxBase.IsHandleCreated); + } + +#pragma warning disable xUnit1026 // Disable xUnit1026 warning: The method doesn't use parameters 'expectedStart' and 'expectedEnd' + [WinFormsTheory] + [MemberData(nameof(TextBoxBaseUiaTextProvider_GetVisibleRangePoints_ForMultilineTextBox_TestData))] + public void TextBoxBaseUiaTextProvider_GetVisibleRangePoints_ReturnsZeros_WithoutHandle(Size size, int expectedStart, int expectedEnd) + { + using SubTextBoxBase textBoxBase = new SubTextBoxBase() { Size = size }; + textBoxBase.Text = "Some test text for testing"; + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + provider.GetVisibleRangePoints(out int providerVisibleStart, out int providerVisibleEnd); + + Assert.Equal(0, providerVisibleStart); + Assert.Equal(0, providerVisibleEnd); + Assert.False(textBoxBase.IsHandleCreated); + } +#pragma warning restore xUnit1026 + + public static IEnumerable TextBoxBaseUiaTextProvider_GetVisibleRanges_TestData() + { + yield return new object[] { new Size(0, 0) }; + yield return new object[] { new Size(100, 20) }; + } + + [WinFormsTheory] + [MemberData(nameof(TextBoxBaseUiaTextProvider_GetVisibleRanges_TestData))] + public void TextBoxBaseUiaTextProvider_GetVisibleRanges_ReturnsCorrectValue(Size size) + { + using SubTextBoxBase textBoxBase = new SubTextBoxBase() + { + Text = "Some test text for testing", + Size = size + }; + textBoxBase.CreateControl(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + Assert.NotNull(provider.GetVisibleRanges()); + Assert.True(textBoxBase.IsHandleCreated); + } + + [WinFormsTheory] + [MemberData(nameof(TextBoxBaseUiaTextProvider_GetVisibleRanges_TestData))] + public void TextBoxBaseUiaTextProvider_GetVisibleRanges_ReturnsNull_WithoutHandle(Size size) + { + using SubTextBoxBase textBoxBase = new SubTextBoxBase() + { + Text = "Some test text for testing", + Size = size + }; + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + Assert.Null(provider.GetVisibleRanges()); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_RangeFromAnnotation_DoesntThrowAnException() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + // RangeFromAnnotation doesn't throw an exception + UiaCore.ITextRangeProvider range = provider.RangeFromAnnotation(textBoxBase.AccessibilityObject); + // RangeFromAnnotation implementation can be changed so this test can be changed too + Assert.NotNull(range); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_RangeFromChild_DoesntThrowAnException() + { + using (new NoAssertContext()) + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + // RangeFromChild doesn't throw an exception + UiaCore.ITextRangeProvider range = provider.RangeFromChild(textBoxBase.AccessibilityObject); + // RangeFromChild implementation can be changed so this test can be changed too + Assert.Null(range); + } + } + + public static IEnumerable TextBoxBaseUiaTextProvider_RangeFromPoint_TestData() + { + yield return new object[] { Point.Empty }; + yield return new object[] { new Point(10, 10) }; + } + + [WinFormsTheory] + [MemberData(nameof(TextBoxBaseUiaTextProvider_RangeFromPoint_TestData))] + public void TextBoxBaseUiaTextProvider_RangeFromPoint_DoesntThrowAnException(Point point) + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + textBoxBase.CreateControl(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + UiaTextRange textRangeProvider = provider.RangeFromPoint(point) as UiaTextRange; + Assert.NotNull(textRangeProvider); + + Assert.True(textBoxBase.IsHandleCreated); + } + + [WinFormsTheory] + [MemberData(nameof(TextBoxBaseUiaTextProvider_RangeFromPoint_TestData))] + public void TextBoxBaseUiaTextProvider_RangeFromPoint_ReturnsNull_WithoutHandle(Point point) + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + UiaTextRange textRangeProvider = provider.RangeFromPoint(point) as UiaTextRange; + Assert.Null(textRangeProvider); + + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData(2, 5)] + [InlineData(0, 10)] + public void TextBoxBaseUiaTextProvider_SetSelection_GetSelection_ReturnCorrectValue(int start, int end) + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + textBoxBase.CreateControl(); + textBoxBase.Text = "Some test text for testing"; + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + provider.SetSelection(start, end); + UiaCore.ITextRangeProvider[] selection = provider.GetSelection(); + Assert.NotNull(selection); + + UiaTextRange textRange = selection[0] as UiaTextRange; + Assert.NotNull(textRange); + + Assert.Equal(start, textRange.Start); + Assert.Equal(end, textRange.End); + + Assert.True(textBoxBase.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData(2, 5)] + [InlineData(0, 10)] + public void TextBoxBaseUiaTextProvider_SetSelection_GetSelection_DontWork_WithoutHandle(int start, int end) + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + textBoxBase.Text = "Some test text for testing"; + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + provider.SetSelection(start, end); + Assert.False(textBoxBase.IsHandleCreated); + UiaCore.ITextRangeProvider[] selection = provider.GetSelection(); + Assert.Null(selection); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData(-5, 10)] + [InlineData(5, 100)] + public void TextBoxBaseUiaTextProvider_SetSelection_DoesntSelectText_IfIncorrectArguments(int start, int end) + { + using (new NoAssertContext()) + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + textBoxBase.CreateControl(); + textBoxBase.Text = "Some test text for testing"; + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + provider.SetSelection(start, end); + UiaCore.ITextRangeProvider[] selection = provider.GetSelection(); + Assert.NotNull(selection); + + UiaTextRange textRange = selection[0] as UiaTextRange; + Assert.NotNull(textRange); + + Assert.Equal(0, textRange.Start); + Assert.Equal(0, textRange.End); + + Assert.True(textBoxBase.IsHandleCreated); + } + } + + [WinFormsTheory] + [InlineData(0)] + [InlineData(2)] + public void TextBoxBaseUiaTextProvider_LineScroll_ReturnCorrectValue(int expectedLine) + { + using TextBoxBase textBoxBase = new SubTextBoxBase + { + Multiline = true, + Text = "Some long long test text for testing GetFirstVisibleLine method", + Size = new Size(50, 100) + }; + textBoxBase.CreateControl(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + + Assert.True(provider.LineScroll(0, expectedLine)); + Assert.Equal(expectedLine, provider.FirstVisibleLine); + Assert.True(textBoxBase.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData(0)] + [InlineData(2)] + public void TextBoxBaseUiaTextProvider_LineScroll_DoesntWork_WitoutHandle(int expectedLine) + { + using TextBoxBase textBoxBase = new SubTextBoxBase + { + Multiline = true, + Size = new Size(50, 100) + }; + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + textBoxBase.Text = "Some long long test text for testing GetFirstVisibleLine method"; + + Assert.False(provider.LineScroll(0, expectedLine)); + Assert.Equal(-1, provider.FirstVisibleLine); + Assert.False(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_GetLogfont_ReturnSegoe_ByDefault() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + Assert.NotEqual(IntPtr.Zero, textBoxBase.Handle); + + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + LOGFONTW logFont = provider.Logfont; + string actual = logFont.FaceName.ToString(); + Assert.Equal("Segoe UI", actual); + Assert.True(textBoxBase.IsHandleCreated); + } + + [WinFormsFact] + public void TextBoxBaseUiaTextProvider_GetLogfont_ReturnEmpty_WithoutHandle() + { + using TextBoxBase textBoxBase = new SubTextBoxBase(); + TextBoxBaseUiaTextProvider provider = new TextBoxBaseUiaTextProvider(textBoxBase); + Assert.False(textBoxBase.IsHandleCreated); + + Assert.Equal(new LOGFONTW(), provider.Logfont); + } + + private class SubTextBoxBase : TextBoxBase + { } + } +} diff --git a/src/System.Windows.Forms/tests/UnitTests/TextBoxBaseTests.cs b/src/System.Windows.Forms/tests/UnitTests/TextBoxBaseTests.cs index 68f039bef8b..d54d70aa0bd 100644 --- a/src/System.Windows.Forms/tests/UnitTests/TextBoxBaseTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/TextBoxBaseTests.cs @@ -1235,8 +1235,7 @@ public void TextBoxBase_Handle_GetMargins_Success(bool multiline, int expected) { Multiline = multiline }; - - Assert.NotEqual(IntPtr.Zero, control.Handle); + control.CreateControl(); IntPtr result = User32.SendMessageW(control.Handle, (User32.WM)User32.EM.GETMARGINS); Assert.Equal(expected, PARAM.LOWORD(result)); Assert.Equal(expected, PARAM.HIWORD(result)); @@ -4873,6 +4872,7 @@ public void TextBoxBase_GetLineFromCharIndex_InvokeNotEmpty_Success(int index) { Text = "text" }; + control.CreateControl(); Assert.Equal(0, control.GetLineFromCharIndex(index)); Assert.True(control.IsHandleCreated); } @@ -4884,7 +4884,7 @@ public void TextBoxBase_GetLineFromCharIndex_InvokeNotEmpty_Success(int index) public void TextBoxBase_GetLineFromCharIndex_InvokeEmptyWithHandle_Success(int index) { using var control = new SubTextBox(); - Assert.NotEqual(IntPtr.Zero, control.Handle); + control.CreateControl(); int invalidatedCallCount = 0; control.Invalidated += (sender, e) => invalidatedCallCount++; int styleChangedCallCount = 0; @@ -4911,7 +4911,7 @@ public void TextBoxBase_GetLineFromCharIndex_InvokeNotEmptyWithHandle_Success(in { Text = "text" }; - Assert.NotEqual(IntPtr.Zero, control.Handle); + control.CreateControl(); int invalidatedCallCount = 0; control.Invalidated += (sender, e) => invalidatedCallCount++; int styleChangedCallCount = 0; @@ -7798,6 +7798,7 @@ public void TextBoxBase_WndProc_InvokeReflectCommandWithHandle_Success(IntPtr wP Assert.Equal(0, styleChangedCallCount); Assert.Equal(0, createdCallCount); } + [WinFormsTheory] [InlineData(true, 3)] [InlineData(false, 0)] @@ -7820,6 +7821,7 @@ public void TextBoxBase_WndProc_InvokeSetFontWithoutHandle_ReturnsExpected(bool Assert.Equal(IntPtr.Zero, m.Result); Assert.Equal(!multiline, control.IsHandleCreated); Assert.Equal(0, textChangedCallCount); + control.CreateControl(); IntPtr result = SendMessageW(control.Handle, (WM)EM.GETMARGINS); Assert.Equal(expectedMargin, PARAM.HIWORD(result)); Assert.Equal(expectedMargin, PARAM.LOWORD(result)); @@ -7977,5 +7979,48 @@ private class SubTextBox : TextBox public new void WndProc(ref Message m) => base.WndProc(ref m); } + + private class SubTextBoxBase : TextBoxBase + { + } + + public static IEnumerable TextBoxBase_GetLineFromCharIndex_TestData() + { + yield return new object[] { new Size(50, 20), false, 0, 0 }; + yield return new object[] { new Size(50, 20), false, 50, 0 }; + yield return new object[] { new Size(100, 50), true, 50, 3 }; + yield return new object[] { new Size(50, 50), true, 50, 8 }; + } + + [WinFormsTheory] + [MemberData(nameof(TextBoxBase_GetLineFromCharIndex_TestData))] + public void TextBoxBase_GetLineFromCharIndex_ReturnsCorrectValue(Size size, bool multiline, int charIndex, int expectedLine) + { + using var textBoxBase = new SubTextBoxBase() { Size = size, Multiline = multiline }; + textBoxBase.Text = "Some test text for testing GetLineFromCharIndex method"; + int actualLine = textBoxBase.GetLineFromCharIndex(charIndex); + Assert.Equal(expectedLine, actualLine); + } + + public static IEnumerable TextBoxBase_GetPositionFromCharIndex_TestData() + { + yield return new object[] { new Size(50, 20), false, 0, new Point(1, 0) }; + yield return new object[] { new Size(50, 20), false, 15, new Point(79, 0) }; + yield return new object[] { new Size(50, 50), true, 12, new Point(14, 31) }; + yield return new object[] { new Size(100, 50), true, 22, new Point(37, 16) }; + yield return new object[] { new Size(50, 50), true, 100, Point.Empty }; + yield return new object[] { new Size(50, 50), true, -1, Point.Empty }; + } + + [WinFormsTheory] + [MemberData(nameof(TextBoxBase_GetPositionFromCharIndex_TestData))] + public void TextBoxBase_GetPositionFromCharIndex_ReturnsCorrectValue(Size size, bool multiline, int charIndex, Point expectedPoint) + { + using var textBoxBase = new SubTextBoxBase() { Size = size, Multiline = multiline }; + textBoxBase.Text = "Some test text for testing GetPositionFromCharIndex method"; + Point actualPoint = textBoxBase.GetPositionFromCharIndex(charIndex); + Assert.True(actualPoint.X >= expectedPoint.X - 1 || actualPoint.X <= expectedPoint.X + 1); + Assert.True(actualPoint.Y >= expectedPoint.Y - 1 || actualPoint.Y <= expectedPoint.Y + 1); + } } } diff --git a/src/System.Windows.Forms/tests/UnitTests/TextBoxTests.cs b/src/System.Windows.Forms/tests/UnitTests/TextBoxTests.cs index acf889537a1..831df8cbabd 100644 --- a/src/System.Windows.Forms/tests/UnitTests/TextBoxTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/TextBoxTests.cs @@ -455,13 +455,14 @@ public void TextBox_CreateAccessibilityInstance_Invoke_ReturnsExpected(bool crea } Assert.Equal(createControl, control.IsHandleCreated); - Control.ControlAccessibleObject instance = Assert.IsType(control.CreateAccessibilityInstance()); + Control.ControlAccessibleObject instance = Assert.IsType(control.CreateAccessibilityInstance()); Assert.Equal(createControl, control.IsHandleCreated); Assert.NotNull(instance); Assert.Same(control, instance.Owner); Assert.Equal(expectedAccessibleRole, instance.Role); Assert.NotSame(control.CreateAccessibilityInstance(), instance); Assert.NotSame(control.AccessibilityObject, instance); + Assert.Equal(createControl, control.IsHandleCreated); } [WinFormsFact] @@ -471,12 +472,13 @@ public void TextBox_CreateAccessibilityInstance_InvokeWithCustomRole_ReturnsExpe { AccessibleRole = AccessibleRole.HelpBalloon }; - Control.ControlAccessibleObject instance = Assert.IsType(control.CreateAccessibilityInstance()); + Control.ControlAccessibleObject instance = Assert.IsType(control.CreateAccessibilityInstance()); Assert.NotNull(instance); Assert.Same(control, instance.Owner); Assert.Equal(AccessibleRole.HelpBalloon, instance.Role); Assert.NotSame(control.CreateAccessibilityInstance(), instance); Assert.NotSame(control.AccessibilityObject, instance); + Assert.False(control.IsHandleCreated); } [WinFormsFact]