|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Drawing;
using System.Windows.Forms.Automation;
using Windows.Win32.System.Com;
using Windows.Win32.System.Variant;
using Windows.Win32.UI.Accessibility;
namespace System.Windows.Forms;
public abstract partial class TextBoxBase
{
internal sealed unsafe class TextBoxBaseUiaTextProvider : UiaTextProvider
{
private readonly WeakReference<TextBoxBase> _owner;
public TextBoxBaseUiaTextProvider(TextBoxBase owner)
{
_owner = new(owner.OrThrowIfNull());
}
private TextBoxBase? Owner => _owner.TryGetTarget(out TextBoxBase? owner) ? owner : null;
public override HRESULT GetSelection(SAFEARRAY** pRetVal)
{
if (pRetVal is null)
{
return HRESULT.E_POINTER;
}
if (Owner is null || !Owner.IsHandleCreated)
{
*pRetVal = SAFEARRAY.CreateEmpty(VARENUM.VT_UNKNOWN);
return HRESULT.S_OK;
}
// First caret position of a selected text
int start = 0;
// Last caret position of a selected text
int end = 0;
// Returns info about the selected text range.
// If there is no selection, start and end parameters are the position of the caret.
PInvokeCore.SendMessage(Owner, PInvokeCore.EM_GETSEL, ref start, ref end);
ComSafeArrayScope<ITextRangeProvider> result = new(1);
// Adding to the SAFEARRAY adds a reference
using var selection = ComHelpers.GetComScope<ITextRangeProvider>(new UiaTextRange(Owner.AccessibilityObject, this, start, end));
result[0] = selection;
*pRetVal = result;
return HRESULT.S_OK;
}
public override HRESULT GetVisibleRanges(SAFEARRAY** pRetVal)
{
if (pRetVal is null)
{
return HRESULT.E_POINTER;
}
if (Owner is null || !Owner.IsHandleCreated)
{
*pRetVal = SAFEARRAY.CreateEmpty(VARENUM.VT_UNKNOWN);
return HRESULT.S_OK;
}
GetVisibleRangePoints(out int start, out int end);
ComSafeArrayScope<ITextRangeProvider> result = new(1);
// Adding to the SAFEARRAY adds a reference
using var ranges = ComHelpers.GetComScope<ITextRangeProvider>(new UiaTextRange(Owner.AccessibilityObject, this, start, end));
result[0] = ranges;
*pRetVal = result;
return HRESULT.S_OK;
}
public override HRESULT RangeFromChild(IRawElementProviderSimple* childElement, ITextRangeProvider** pRetVal)
{
if (pRetVal is null)
{
return HRESULT.E_POINTER;
}
// We don't have any children so this call returns null.
Debug.Fail("Text edit control cannot have a child element.");
*pRetVal = null;
return HRESULT.S_OK;
}
public override HRESULT RangeFromPoint(UiaPoint point, ITextRangeProvider** pRetVal)
{
if (pRetVal is null)
{
return HRESULT.E_POINTER;
}
if (Owner is null || !Owner.IsHandleCreated)
{
*pRetVal = default;
return HRESULT.S_OK;
}
Point clientLocation = point;
// Convert screen to client coordinates.
// (Essentially ScreenToClient but MapWindowPoints accounts for window mirroring using WS_EX_LAYOUTRTL.)
if (PInvokeCore.MapWindowPoints((HWND)default, Owner, ref clientLocation) == 0)
{
*pRetVal = ComHelpers.GetComPointer<ITextRangeProvider>(
new UiaTextRange(
Owner.AccessibilityObject,
this,
start: 0,
end: 0));
return HRESULT.S_OK;
}
// 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 = Owner.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 = Owner.GetCharIndexFromPosition(clientLocation);
*pRetVal = ComHelpers.GetComPointer<ITextRangeProvider>(
new UiaTextRange(
Owner.AccessibilityObject,
this,
start,
start));
return HRESULT.S_OK;
}
public override Rectangle RectangleToScreen(Rectangle rect) => Owner is not null ? Owner.RectangleToScreen(rect) : Rectangle.Empty;
public override ITextRangeProvider* DocumentRange => Owner is not null
? ComHelpers.GetComPointer<ITextRangeProvider>(
new UiaTextRange(
Owner.AccessibilityObject,
this,
start: 0,
TextLength))
: null;
public override SupportedTextSelection SupportedTextSelection => SupportedTextSelection.SupportedTextSelection_Single;
public override HRESULT GetCaretRange(BOOL* isActive, ITextRangeProvider** pRetVal)
{
if (isActive is null || pRetVal is null)
{
return HRESULT.E_POINTER;
}
*isActive = false;
if (Owner is null || !Owner.IsHandleCreated)
{
*pRetVal = null;
return HRESULT.S_OK;
}
VARIANT hasKeyboardFocus = Owner.AccessibilityObject.GetPropertyValue(UIA_PROPERTY_ID.UIA_HasKeyboardFocusPropertyId);
*isActive = !hasKeyboardFocus.IsEmpty && (bool)hasKeyboardFocus;
*pRetVal = ComHelpers.GetComPointer<ITextRangeProvider>(
new UiaTextRange(
Owner.AccessibilityObject,
this,
Owner.SelectionStart,
Owner.SelectionStart));
return HRESULT.S_OK;
}
public override Point PointToScreen(Point pt) => Owner is not null ? Owner.PointToScreen(pt) : Point.Empty;
public override HRESULT RangeFromAnnotation(IRawElementProviderSimple* annotationElement, ITextRangeProvider** pRetVal)
{
if (pRetVal is null)
{
return HRESULT.E_POINTER;
}
*pRetVal = Owner is not null
? ComHelpers.GetComPointer<ITextRangeProvider>(
new UiaTextRange(
Owner.AccessibilityObject,
this,
start: 0,
end: 0))
: null;
return HRESULT.S_OK;
}
public override Rectangle BoundingRectangle
=> Owner is not null && Owner.IsHandleCreated
? GetFormattingRectangle()
: Rectangle.Empty;
public override int FirstVisibleLine
=> Owner is not null && Owner.IsHandleCreated
? (int)PInvokeCore.SendMessage(Owner, PInvokeCore.EM_GETFIRSTVISIBLELINE)
: -1;
public override bool IsMultiline => Owner is not null && Owner.Multiline;
public override bool IsReadingRTL
=> Owner is not null && Owner.IsHandleCreated && WindowExStyle.HasFlag(WINDOW_EX_STYLE.WS_EX_RTLREADING);
public override bool IsReadOnly => Owner is not null && Owner.ReadOnly;
public override bool IsScrollable
{
get
{
if (Owner is null || !Owner.IsHandleCreated)
{
return false;
}
return ((int)GetWindowStyle(Owner) & PInvoke.ES_AUTOHSCROLL) != 0;
}
}
public override int LinesCount
=> Owner is not null && Owner.IsHandleCreated
? (int)PInvokeCore.SendMessage(Owner, PInvokeCore.EM_GETLINECOUNT)
: -1;
public override int LinesPerPage
{
get
{
if (Owner is null || !Owner.IsHandleCreated)
{
return -1;
}
Rectangle rect = Owner.ClientRectangle;
if (rect.IsEmpty)
{
return 0;
}
if (!Owner.Multiline)
{
return 1;
}
int fontHeight = Owner.Font.Height;
return fontHeight != 0 ? (int)Math.Ceiling(((double)rect.Height) / fontHeight) : 0;
}
}
public override LOGFONTW Logfont
=> Owner is not null && Owner.IsHandleCreated
? Owner.Font.ToLogicalFont()
: default;
public override string Text
=> Owner is not null && Owner.IsHandleCreated
? Owner.PasswordProtect
? PasswordString
: Owner.Text
: string.Empty;
private string PasswordString
=> Owner is not null
? new string(Owner is TextBox textBox
? textBox.PasswordChar
: ((MaskedTextBox)Owner).PasswordChar, Owner.Text.Length)
: string.Empty;
public override int TextLength => Text.Length;
public override WINDOW_EX_STYLE WindowExStyle
=> Owner is not null && Owner.IsHandleCreated
? GetWindowExStyle(Owner)
: WINDOW_EX_STYLE.WS_EX_LEFT;
public override WINDOW_STYLE WindowStyle
=> Owner is not null && Owner.IsHandleCreated
? GetWindowStyle(Owner)
: WINDOW_STYLE.WS_OVERLAPPED;
public override int GetLineFromCharIndex(int charIndex)
=> Owner is not null && Owner.IsHandleCreated
? Owner.GetLineFromCharIndex(charIndex)
: -1;
public override int GetLineIndex(int line)
=> Owner is not null && Owner.IsHandleCreated
? (int)PInvokeCore.SendMessage(Owner, PInvokeCore.EM_LINEINDEX, (WPARAM)line)
: -1;
public override Point GetPositionFromChar(int charIndex)
=> Owner is not null && Owner.IsHandleCreated
? Owner.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 (Owner is null || !Owner.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 Owner.GetPositionFromCharIndex(useNext ? startCharIndex + 1 : startCharIndex);
}
pt = Owner.GetPositionFromCharIndex(startCharIndex);
if (ch is '\r' or '\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 = Owner.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 (Owner is null || !Owner.IsHandleCreated || IsDegenerate(Owner.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(rectangle.X + 1, rectangle.Y + 1);
Point ptEnd = new(rectangle.Right - 1, rectangle.Bottom - 1);
if (IsMultiline)
{
visibleStart = GetLineIndex(FirstVisibleLine);
int lastVisibleLine = FirstVisibleLine + LinesPerPage - 1;
// Index of the next line is the end caret position of the previous line.
visibleEnd = GetLineIndex(lastVisibleLine + 1);
if (visibleEnd == -1)
{
visibleEnd = Text.Length;
}
}
else if (Owner is not null)
{
visibleStart = Owner.GetCharIndexFromPosition(ptStart);
// Add 1 to get a caret position after received character.
visibleEnd = Owner.GetCharIndexFromPosition(ptEnd) + 1;
}
return;
static 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.
=> Owner is not null && Owner.IsHandleCreated
&& PInvokeCore.SendMessage(
Owner,
PInvokeCore.EM_LINESCROLL,
(WPARAM)charactersHorizontal,
(LPARAM)linesVertical) != 0;
public override void SetSelection(int start, int end)
{
if (Owner is null || !Owner.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;
}
PInvokeCore.SendMessage(Owner, PInvokeCore.EM_SETSEL, (WPARAM)start, (LPARAM)end);
}
private RECT GetFormattingRectangle()
{
if (Owner is null)
{
return default;
}
Debug.Assert(Owner.IsHandleCreated);
// Send an EM_GETRECT message to find out the bounding rectangle.
RECT rectangle = default;
PInvokeCore.SendMessage(Owner, PInvokeCore.EM_GETRECT, (WPARAM)0, ref rectangle);
return rectangle;
}
private unsafe bool GetTextExtentPoint32(char item, out Size size)
{
if (Owner is null)
{
size = Size.Empty;
return false;
}
Debug.Assert(Owner.IsHandleCreated);
size = default;
using GetDcScope hdc = new(Owner.HWND);
if (hdc.IsNull)
{
return false;
}
fixed (void* pSize = &size)
{
// Add the width of the character at that position.
return PInvoke.GetTextExtentPoint32W(hdc, &item, 1, (SIZE*)pSize);
}
}
}
}
|