using System.Runtime.InteropServices; using System.Windows.Automation; using System.Windows.Automation.Text; using Accessibility; namespace CursorLang.Interop; /// /// Определяет положение каретки в активном поле ввода — в том числе в чужом приложении. /// /// /// Единого способа нет: классические Win32-приложения заводят системную каретку, /// а Chrome, Electron и прочие рисуют её сами и сообщают положение только через /// средства доступности. Поэтому сначала спрашиваем систему, затем — приложение. /// internal static class CaretNative { [StructLayout(LayoutKind.Sequential)] private struct GuiThreadInfo { public int cbSize; public uint flags; public IntPtr hwndActive; public IntPtr hwndFocus; public IntPtr hwndCapture; public IntPtr hwndMenuOwner; public IntPtr hwndMoveSize; public IntPtr hwndCaret; public PopupWindowNative.Rect rcCaret; } [DllImport("user32.dll")] private static extern bool GetGUIThreadInfo(uint idThread, ref GuiThreadInfo lpgui); [DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr lpdwProcessId); [DllImport("user32.dll")] private static extern bool ClientToScreen(IntPtr hWnd, ref PopupWindowNative.Point lpPoint); [DllImport("oleacc.dll")] private static extern int AccessibleObjectFromWindow(IntPtr hwnd, uint dwObjectId, ref Guid riid, out IAccessible ppvObject); private const uint OBJID_CARET = 0xFFFFFFF8; private const int CHILDID_SELF = 0; /// /// Прямоугольник каретки в пикселях экрана или null, если активное /// приложение не сообщает её положение. /// internal static PopupWindowNative.Rect? TryGetCaretRect() { var info = new GuiThreadInfo { cbSize = Marshal.SizeOf() }; uint threadId = GetWindowThreadProcessId(GetForegroundWindow(), IntPtr.Zero); if (!GetGUIThreadInfo(threadId, ref info)) { return null; } return TryGetSystemCaret(info) ?? TryGetAccessibleCaret(info.hwndFocus) ?? TryGetAutomationCaret(); } /// Сколько ждём ответа от чужого приложения по UI Automation. private static readonly TimeSpan AutomationTimeout = TimeSpan.FromMilliseconds(150); // Браузеры и другие приложения на своих движках рисуют каретку сами и // сообщают её положение только через UI Automation. Запрос идёт в чужой // процесс, поэтому он самый медленный и стоит последним private static PopupWindowNative.Rect? TryGetAutomationCaret() { // Зависшее приложение не должно подвешивать подсказку вместе с собой: // ждём ответ ограниченное время, иначе показываем у курсора Task query = Task.Run(QueryAutomationCaret); return query.Wait(AutomationTimeout) ? query.Result : null; } private static PopupWindowNative.Rect? QueryAutomationCaret() { try { AutomationElement focused = AutomationElement.FocusedElement; if (focused is null || !focused.TryGetCurrentPattern(TextPattern.Pattern, out object pattern)) { return null; } TextPatternRange[] selection = ((TextPattern)pattern).GetSelection(); if (selection.Length == 0) { return null; } // У каретки выделение пустое и прямоугольника не имеет, // поэтому расширяем его до ближайшего символа TextPatternRange range = selection[0].Clone(); range.ExpandToEnclosingUnit(TextUnit.Character); System.Windows.Rect[] rectangles = range.GetBoundingRectangles(); if (rectangles.Length == 0 || rectangles[0].Height <= 0) { return null; } System.Windows.Rect caret = rectangles[0]; return new PopupWindowNative.Rect { Left = (int)caret.Left, Top = (int)caret.Top, Right = (int)caret.Right, Bottom = (int)caret.Bottom, }; } catch (Exception e) when (e is ElementNotAvailableException or InvalidOperationException or COMException) { // Приложение закрылось или не отвечает — подсказку это ронять не должно return null; } } // Системная каретка: координаты приходят относительно окна, которому она принадлежит private static PopupWindowNative.Rect? TryGetSystemCaret(GuiThreadInfo info) { if (info.hwndCaret == IntPtr.Zero || IsEmpty(info.rcCaret)) { return null; } var topLeft = new PopupWindowNative.Point { X = info.rcCaret.Left, Y = info.rcCaret.Top }; var bottomRight = new PopupWindowNative.Point { X = info.rcCaret.Right, Y = info.rcCaret.Bottom }; if (!ClientToScreen(info.hwndCaret, ref topLeft) || !ClientToScreen(info.hwndCaret, ref bottomRight)) { return null; } return new PopupWindowNative.Rect { Left = topLeft.X, Top = topLeft.Y, Right = bottomRight.X, Bottom = bottomRight.Y, }; } // Каретка через средства доступности: сюда попадают браузеры и Electron private static PopupWindowNative.Rect? TryGetAccessibleCaret(IntPtr hwndFocus) { if (hwndFocus == IntPtr.Zero) { return null; } Guid iid = typeof(IAccessible).GUID; if (AccessibleObjectFromWindow(hwndFocus, OBJID_CARET, ref iid, out IAccessible caret) != 0) { return null; } try { caret.accLocation(out int left, out int top, out int width, out int height, CHILDID_SELF); var rect = new PopupWindowNative.Rect { Left = left, Top = top, Right = left + width, Bottom = top + height, }; return IsEmpty(rect) ? null : rect; } catch (COMException) { // Приложение объявило поддержку, но положение не отдало return null; } finally { Marshal.ReleaseComObject(caret); } } // Когда каретки нет, её прямоугольник приходит нулевой высоты. // Судим только по высоте: нулевые координаты — это обычное начало пустого поля private static bool IsEmpty(PopupWindowNative.Rect rect) => rect.Bottom - rect.Top <= 0; }