File: System\Windows\Forms\Controls\ComboBox\ComboBox.ComboBoxUiaTextProvider.cs
Web Access
Project: src\src\System.Windows.Forms\src\System.Windows.Forms.csproj (System.Windows.Forms)
// 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 partial class ComboBox
{
    internal sealed unsafe class ComboBoxUiaTextProvider : UiaTextProvider
    {
        /// <summary>
        ///  Since the TextBox inside the ComboBox is always single-line, for optimization
        ///  we always return 0 as the index of lines
        /// </summary>
        private const int OwnerChildEditLineIndex = 0;
 
        /// <summary>
        ///  Since the TextBox inside the ComboBox is always single-line, for optimization
        ///  we always return 1 as the number of lines
        /// </summary>
        private const int OwnerChildEditLinesCount = 1;
 
        private readonly IHandle<HWND> _owningChildEdit;
 
        private readonly ComboBox _owningComboBox;
 
        public ComboBoxUiaTextProvider(ComboBox owner)
        {
            _owningComboBox = owner.OrThrowIfNull();
            Debug.Assert(_owningComboBox.IsHandleCreated);
 
            _owningChildEdit = owner._childEdit!;
        }
 
        public override Rectangle BoundingRectangle
            => _owningComboBox.IsHandleCreated
                ? GetFormattingRectangle()
                : Rectangle.Empty;
 
        public override ITextRangeProvider* DocumentRange
            => ComHelpers.GetComPointer<ITextRangeProvider>(
                new UiaTextRange(
                    _owningComboBox.ChildEditAccessibleObject,
                    this,
                    start:
                    0,
                    TextLength));
 
        public override int FirstVisibleLine
            => _owningComboBox.IsHandleCreated
                ? 0
                : -1;
 
        public override bool IsMultiline => false;
 
        public override bool IsReadingRTL
            => _owningComboBox.IsHandleCreated && WindowExStyle.HasFlag(WINDOW_EX_STYLE.WS_EX_RTLREADING);
 
        public override bool IsReadOnly => false;
 
        public override bool IsScrollable
        {
            get
            {
                if (!_owningComboBox.IsHandleCreated)
                {
                    return false;
                }
 
                return ((int)GetWindowStyle(_owningChildEdit) & PInvoke.ES_AUTOHSCROLL) != 0;
            }
        }
 
        public override int LinesCount
            => _owningComboBox.IsHandleCreated
                ? OwnerChildEditLinesCount
                : -1;
 
        public override int LinesPerPage
            => !_owningComboBox.IsHandleCreated
                    ? -1
                    : _owningComboBox.ChildEditAccessibleObject.BoundingRectangle.IsEmpty
                        ? 0
                        : OwnerChildEditLinesCount;
 
        public override LOGFONTW Logfont
            => _owningComboBox.IsHandleCreated
                ? _owningComboBox.Font.ToLogicalFont()
                : default;
 
        public override SupportedTextSelection SupportedTextSelection => SupportedTextSelection.SupportedTextSelection_Single;
 
        public override string Text
            => _owningComboBox.IsHandleCreated
                ? PInvoke.GetWindowText(_owningChildEdit)
                : string.Empty;
 
        public override int TextLength
            => _owningComboBox.IsHandleCreated
                ? (int)PInvoke.SendMessage(_owningChildEdit, PInvoke.WM_GETTEXTLENGTH)
                : -1;
 
        public override WINDOW_EX_STYLE WindowExStyle
            => _owningComboBox.IsHandleCreated
                ? GetWindowExStyle(_owningChildEdit)
                : WINDOW_EX_STYLE.WS_EX_LEFT;
 
        public override WINDOW_STYLE WindowStyle
            => _owningComboBox.IsHandleCreated
                ? GetWindowStyle(_owningChildEdit)
                : WINDOW_STYLE.WS_OVERLAPPED;
 
        public override HRESULT GetCaretRange(BOOL* isActive, ITextRangeProvider** pRetVal)
        {
            if (pRetVal is null || isActive is null)
            {
                return HRESULT.E_POINTER;
            }
 
            *isActive = false;
 
            if (!_owningComboBox.IsHandleCreated)
            {
                *pRetVal = null;
                return HRESULT.S_OK;
            }
 
            object? hasKeyboardFocus = _owningComboBox.ChildEditAccessibleObject.GetPropertyValue(UIA_PROPERTY_ID.UIA_HasKeyboardFocusPropertyId);
            *isActive = hasKeyboardFocus is true;
 
            *pRetVal = ComHelpers.GetComPointer<ITextRangeProvider>(
                new UiaTextRange(
                    _owningComboBox.ChildEditAccessibleObject,
                    this,
                    _owningComboBox.SelectionStart,
                    _owningComboBox.SelectionStart));
            return HRESULT.S_OK;
        }
 
        public override int GetLineFromCharIndex(int charIndex)
            => _owningComboBox.IsHandleCreated
                ? OwnerChildEditLineIndex
                : -1;
 
        public override int GetLineIndex(int line)
            => _owningComboBox.IsHandleCreated
                ? OwnerChildEditLineIndex
                : -1;
 
        public override Point GetPositionFromChar(int charIndex)
            => _owningComboBox.IsHandleCreated
                ? 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 (!_owningComboBox.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 GetPositionFromCharIndex(useNext ? startCharIndex + 1 : startCharIndex);
                }
 
                pt = 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 = 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 HRESULT GetSelection(SAFEARRAY** pRetVal)
        {
            if (pRetVal is null)
            {
                return HRESULT.E_POINTER;
            }
 
            if (!_owningComboBox.IsHandleCreated)
            {
                *pRetVal = null;
                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.
            PInvoke.SendMessage(_owningChildEdit, PInvoke.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(_owningComboBox.ChildEditAccessibleObject, this, start, end));
            result[0] = selection;
 
            *pRetVal = result;
            return HRESULT.S_OK;
        }
 
        public override void GetVisibleRangePoints(out int visibleStart, out int visibleEnd)
        {
            visibleStart = 0;
            visibleEnd = 0;
 
            if (!_owningComboBox.IsHandleCreated || IsDegenerate(_owningComboBox.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);
 
            visibleStart = GetCharIndexFromPosition(ptStart);
            visibleEnd = GetCharIndexFromPosition(ptEnd) + 1; // Add 1 to get a caret position after received character
 
            return;
 
            static bool IsDegenerate(Rectangle rect)
                => rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0;
        }
 
        public override HRESULT GetVisibleRanges(SAFEARRAY** pRetVal)
        {
            if (pRetVal is null)
            {
                return HRESULT.E_POINTER;
            }
 
            if (!_owningComboBox.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(_owningComboBox.ChildEditAccessibleObject, this, start, end));
            result[0] = ranges;
 
            *pRetVal = result;
            return HRESULT.S_OK;
        }
 
        public override bool LineScroll(int charactersHorizontal, int linesVertical)
            // If the EM_LINESCROLL message is sent to a single-line edit control, the return value is FALSE.
            => false;
 
        public override Point PointToScreen(Point pt)
        {
            PInvoke.MapWindowPoints(_owningChildEdit, (HWND)default, ref pt);
            return pt;
        }
 
        public override HRESULT RangeFromAnnotation(IRawElementProviderSimple* annotationElement, ITextRangeProvider** pRetVal)
        {
            if (pRetVal is null)
            {
                return HRESULT.E_POINTER;
            }
 
            *pRetVal = ComHelpers.GetComPointer<ITextRangeProvider>(
                new UiaTextRange(
                    _owningComboBox.ChildEditAccessibleObject,
                    this,
                    start: 0,
                    end: 0));
            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 (!_owningComboBox.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 (PInvoke.MapWindowPoints((HWND)default, _owningChildEdit, ref clientLocation) == 0)
            {
                *pRetVal = ComHelpers.GetComPointer<ITextRangeProvider>(
                    new UiaTextRange(
                        _owningComboBox.ChildEditAccessibleObject,
                        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 = _owningComboBox.ChildEditAccessibleObject.BoundingRectangle;
 
            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 = GetCharIndexFromPosition(clientLocation);
 
            *pRetVal = ComHelpers.GetComPointer<ITextRangeProvider>(
                new UiaTextRange(
                    _owningComboBox.ChildEditAccessibleObject,
                    this,
                    start,
                    start));
            return HRESULT.S_OK;
        }
 
        public override Rectangle RectangleToScreen(Rectangle rect) => _owningComboBox.RectangleToScreen(rect);
 
        public override void SetSelection(int start, int end)
        {
            if (!_owningComboBox.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;
            }
 
            PInvoke.SendMessage(_owningChildEdit, PInvoke.EM_SETSEL, (WPARAM)start, (LPARAM)end);
        }
 
        private int GetCharIndexFromPosition(Point pt)
        {
            int index = (int)PInvoke.SendMessage(_owningChildEdit, PInvoke.EM_CHARFROMPOS, (WPARAM)0, (LPARAM)pt);
            index = PARAM.LOWORD(index);
 
            if (index < 0)
            {
                index = 0;
            }
            else
            {
                string t = Text;
 
                // EM_CHARFROMPOS will return an invalid number if the last character in the RichEdit
                // is a newline.
                if (index >= t.Length)
                {
                    index = Math.Max(t.Length - 1, 0);
                }
            }
 
            return index;
        }
 
        private RECT GetFormattingRectangle()
        {
            // Send an EM_GETRECT message to find out the bounding rectangle.
            RECT rectangle = default;
            PInvoke.SendMessage(_owningChildEdit, PInvoke.EM_GETRECT, (WPARAM)0, ref rectangle);
 
            return rectangle;
        }
 
        private Point GetPositionFromCharIndex(int index)
        {
            if (index < 0 || index >= Text.Length)
            {
                return Point.Empty;
            }
 
            int i = (int)PInvoke.SendMessage(_owningChildEdit, PInvoke.EM_POSFROMCHAR, (WPARAM)index);
 
            return new Point(PARAM.SignedLOWORD(i), PARAM.SignedHIWORD(i));
        }
 
        private unsafe bool GetTextExtentPoint32(char item, out Size size)
        {
            size = default;
 
            using GetDcScope hdc = new(_owningChildEdit.Handle);
            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);
            }
        }
    }
}