fixed comment into code

This commit is contained in:
2026-08-09 20:02:23 +05:00
parent 778fc1034b
commit 7e4e569d53
34 changed files with 332 additions and 306 deletions
+26 -26
View File
@@ -6,12 +6,12 @@ using Accessibility;
namespace CursorLang.Interop;
/// <summary>
/// Определяет положение каретки в активном поле ввода — в том числе в чужом приложении.
/// Locates the caret in the active input field — including one in another application.
/// </summary>
/// <remarks>
/// Единого способа нет: классические Win32-приложения заводят системную каретку,
/// а Chrome, Electron и прочие рисуют её сами и сообщают положение только через
/// средства доступности. Поэтому сначала спрашиваем систему, затем — приложение.
/// There is no single way to do it: classic Win32 applications create a system caret,
/// while Chrome, Electron and others draw it themselves and report its position only
/// through accessibility interfaces. So we ask the system first, then the application.
/// </remarks>
internal static class CaretNative
{
@@ -32,8 +32,8 @@ internal static class CaretNative
private const int CHILDID_SELF = 0;
/// <summary>
/// Прямоугольник каретки в пикселях экрана или <c>null</c>, если активное
/// приложение не сообщает её положение.
/// The caret rectangle in screen pixels, or <c>null</c> when the active
/// application does not report its position.
/// </summary>
internal static PopupWindowNative.Rect? TryGetCaretRect()
{
@@ -50,13 +50,13 @@ internal static class CaretNative
}
/// <summary>
/// Отсеивает заведомо неверные координаты.
/// Filters out obviously wrong coordinates.
/// </summary>
/// <remarks>
/// Часть приложений отдаёт положение каретки в своей системе координат либо
/// без учёта масштаба экрана, и подсказка уезжает далеко от поля ввода.
/// Каретка обязана находиться внутри окна ввода — это и проверяем, а перед
/// отказом пробуем истолковать координаты как немасштабированные.
/// Some applications report the caret position in their own coordinate system or
/// without accounting for display scaling, and the popup ends up far from the input
/// field. The caret must be inside the input window — that is what we check, and
/// before giving up we try to read the coordinates as unscaled ones.
/// </remarks>
private static PopupWindowNative.Rect? Validate(PopupWindowNative.Rect caret, IntPtr hwndFocus)
{
@@ -86,16 +86,16 @@ internal static class CaretNative
inner.Left >= outer.Left && inner.Right <= outer.Right &&
inner.Top >= outer.Top && inner.Bottom <= outer.Bottom;
/// <summary>Сколько ждём ответа от чужого приложения по UI Automation.</summary>
/// <summary>How long we wait for another application to answer over UI Automation.</summary>
private static readonly TimeSpan AutomationTimeout = TimeSpan.FromMilliseconds(150);
// Браузеры и другие приложения на своих движках рисуют каретку сами и
// сообщают её положение только через UI Automation. Запрос идёт в чужой
// процесс, поэтому он самый медленный и стоит последним
// Browsers and other applications with their own rendering engines draw the caret
// themselves and report its position only through UI Automation. The request goes
// into another process, so it is the slowest one and comes last
private static PopupWindowNative.Rect? TryGetAutomationCaret()
{
// Зависшее приложение не должно подвешивать подсказку вместе с собой:
// ждём ответ ограниченное время, иначе показываем у курсора
// A hung application must not hang the popup along with it: we wait for the
// answer for a limited time, otherwise we show the popup at the cursor
Task<PopupWindowNative.Rect?> query = Task.Run(QueryAutomationCaret);
return query.Wait(AutomationTimeout) ? query.Result : null;
}
@@ -117,8 +117,8 @@ internal static class CaretNative
return null;
}
// У каретки выделение пустое и прямоугольника не имеет,
// поэтому расширяем его до ближайшего символа
// The caret has an empty selection and therefore no rectangle,
// so we expand it to the nearest character
TextPatternRange range = selection[0].Clone();
range.ExpandToEnclosingUnit(TextUnit.Character);
@@ -141,12 +141,12 @@ internal static class CaretNative
or InvalidOperationException
or COMException)
{
// Приложение закрылось или не отвечает — подсказку это ронять не должно
// The application closed or stopped responding — that must not take the popup down
return null;
}
}
// Системная каретка: координаты приходят относительно окна, которому она принадлежит
// The system caret: its coordinates come relative to the window that owns it
private static PopupWindowNative.Rect? TryGetSystemCaret(ForegroundInputNative.GuiThreadInfo info)
{
if (info.hwndCaret == IntPtr.Zero || IsEmpty(info.rcCaret))
@@ -170,7 +170,7 @@ internal static class CaretNative
};
}
// Каретка через средства доступности: сюда попадают браузеры и Electron
// The caret through accessibility interfaces: this is where browsers and Electron land
private static PopupWindowNative.Rect? TryGetAccessibleCaret(IntPtr hwndFocus)
{
if (hwndFocus == IntPtr.Zero)
@@ -199,7 +199,7 @@ internal static class CaretNative
}
catch (COMException)
{
// Приложение объявило поддержку, но положение не отдало
// The application declared support but did not report the position
return null;
}
finally
@@ -208,7 +208,7 @@ internal static class CaretNative
}
}
// Когда каретки нет, её прямоугольник приходит нулевой высоты.
// Судим только по высоте: нулевые координаты — это обычное начало пустого поля
private static bool IsEmpty(PopupWindowNative.Rect rect) => rect.Bottom - rect.Top <= 0;
// When there is no caret, its rectangle comes back with zero height.
// We judge by height alone: zero coordinates are a normal start of an empty field
internal static bool IsEmpty(PopupWindowNative.Rect rect) => rect.Bottom - rect.Top <= 0;
}
+6 -6
View File
@@ -3,8 +3,8 @@ using System.Runtime.InteropServices;
namespace CursorLang.Interop;
/// <summary>
/// Сведения о вводе в активном приложении: какое окно держит фокус клавиатуры
/// и где находится каретка.
/// Input details of the active application: which window holds keyboard focus
/// and where the caret is.
/// </summary>
internal static class ForegroundInputNative
{
@@ -26,12 +26,12 @@ internal static class ForegroundInputNative
private static extern bool GetGUIThreadInfo(uint idThread, ref GuiThreadInfo lpgui);
/// <summary>
/// Состояние ввода потока переднего плана.
/// The input state of the foreground thread.
/// </summary>
/// <remarks>
/// Нулевой идентификатор потока не случаен: у современных приложений окно
/// верхнего уровня и окно ввода живут в разных потоках, и спрашивать нужно
/// именно про передний план целиком.
/// The zero thread identifier is not accidental: in modern applications the
/// top-level window and the input window live in different threads, and the
/// question has to be about the foreground as a whole.
/// </remarks>
internal static bool TryGetInfo(out GuiThreadInfo info)
{
@@ -3,21 +3,21 @@ using System.Runtime.InteropServices;
namespace CursorLang.Interop;
/// <summary>
/// Право выводить окно на передний план.
/// The right to bring a window to the foreground.
/// </summary>
internal static class ForegroundPermissionNative
{
/// <summary>ASFW_ANY — право получает любой процесс.</summary>
/// <summary>ASFW_ANY — any process gets the right.</summary>
private const uint AnyProcess = 0xFFFFFFFF;
/// <summary>
/// Уступает своё право вывести окно на передний план другим процессам.
/// Gives up our right to bring a window to the foreground in favour of other processes.
/// </summary>
/// <remarks>
/// Поменять окно переднего плана Windows разрешает не всякому: нужно быть
/// процессом, с которым пользователь работал последним. Давно запущенный
/// экземпляр приложения к таким не относится, и его окно всплывёт только
/// если правом поделится процесс, который пользователь запустил только что.
/// Windows does not let just anyone change the foreground window: you have to be
/// the process the user interacted with last. An instance started long ago is not
/// one of those, and its window will come up only if the right is shared by the
/// process the user has just launched.
/// </remarks>
internal static void GrantToAnyProcess() => AllowSetForegroundWindow(AnyProcess);
+17 -17
View File
@@ -3,16 +3,16 @@ using System.Runtime.InteropServices;
namespace CursorLang.Interop;
/// <summary>
/// Win32 API для определения раскладки активного приложения.
/// Win32 API for reading the layout of the active application.
/// </summary>
internal static class KeyboardLayoutNative
{
private const uint WmInputLangChangeRequest = 0x0050;
/// <summary>Взять следующую раскладку из системного списка.</summary>
/// <summary>Take the next layout from the system list.</summary>
private static readonly IntPtr InputLangChangeForward = new(0x0002);
/// <summary>HKL_NEXT — та же просьба на языке старых версий Windows.</summary>
/// <summary>HKL_NEXT — the same request in the language of older Windows versions.</summary>
private static readonly IntPtr HklNext = new(1);
[DllImport("user32.dll")]
@@ -28,17 +28,17 @@ internal static class KeyboardLayoutNative
private static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
/// <summary>
/// Раскладка, которой сейчас печатает пользователь.
/// The layout the user is currently typing with.
/// </summary>
internal static int GetActiveLocaleId() => GetLocaleIdOf(GetInputWindow());
/// <summary>
/// Просит активное приложение перейти на следующую раскладку из системного списка.
/// Asks the active application to switch to the next layout from the system list.
/// </summary>
/// <remarks>
/// Синтезировать системное сочетание вроде Alt+Shift не годится: его назначение
/// пользователь меняет в настройках Windows и может отключить вовсе. Просьба
/// сообщением от таких настроек не зависит и работает в чужом процессе.
/// Synthesizing a system shortcut such as Alt+Shift will not do: the user can
/// reassign it in the Windows settings or turn it off entirely. A request sent as
/// a message does not depend on those settings and works in another process.
/// </remarks>
internal static void RequestNextLayout()
{
@@ -48,19 +48,19 @@ internal static class KeyboardLayoutNative
return;
}
// Оба параметра означают одно и то же: разные версии Windows и разные
// библиотеки интерфейса смотрят то на флаг, то на lParam
// Both parameters mean the same thing: different Windows versions and different
// UI frameworks look either at the flag or at lParam
PostMessage(target, WmInputLangChangeRequest, InputLangChangeForward, HklNext);
}
/// <summary>
/// Окно, которому принадлежит ввод с клавиатуры.
/// The window that owns keyboard input.
/// </summary>
/// <remarks>
/// Спрашиваем окно с фокусом клавиатуры, а не окно верхнего плана: у Блокнота
/// Windows 11, меню «Пуск» и прочих приложений на WinUI поле ввода живёт в
/// отдельном потоке, и раскладка меняется только у него. У потока главного
/// окна она остаётся прежней, и переключение проходит незамеченным.
/// We ask the window with keyboard focus rather than the foreground window: in
/// Windows 11 Notepad, the Start menu and other WinUI applications the input field
/// lives in a separate thread, and the layout changes only for that thread. For the
/// main window's thread it stays the same, and the switch goes unnoticed.
/// </remarks>
private static IntPtr GetInputWindow()
{
@@ -74,8 +74,8 @@ internal static class KeyboardLayoutNative
}
/// <summary>
/// Идентификатор локали для окна. Раскладка в Windows привязана к потоку,
/// поэтому так её видно у любого приложения, а не только у своего.
/// The locale identifier for a window. In Windows the layout is bound to a thread,
/// so this reveals it for any application, not only for our own.
/// </summary>
internal static int GetLocaleIdOf(IntPtr hWnd)
{
+19 -19
View File
@@ -3,22 +3,22 @@ using System.Runtime.InteropServices;
namespace CursorLang.Interop;
/// <summary>
/// Системный перехватчик клавиатуры (WH_KEYBOARD_LL): видит нажатия во всех
/// приложениях и умеет не пропускать их дальше.
/// A system keyboard hook (WH_KEYBOARD_LL): sees key presses in every application
/// and can keep them from going any further.
/// </summary>
/// <remarks>
/// Ставится только низкоуровневый хук: обычный WH_KEYBOARD требует внедрения
/// DLL в чужие процессы, что для managed-кода невозможно. Обратный вызов
/// приходит на тот поток, который поставил хук, и поток обязан крутить цикл
/// сообщений — отсюда требование ставить хук из потока пользовательского
/// интерфейса. Возврат управления затягивать нельзя: по истечении системного
/// тайм-аута Windows молча снимает хук.
/// Only the low-level hook is installed: a regular WH_KEYBOARD requires injecting a
/// DLL into other processes, which is impossible for managed code. The callback
/// arrives on the thread that installed the hook, and that thread must pump a message
/// loop — hence the requirement to install the hook from the user interface thread.
/// Returning control must not be delayed: once the system timeout expires, Windows
/// silently removes the hook.
/// </remarks>
internal sealed class LowLevelKeyboardHook : IDisposable
{
/// <summary>
/// Обработчик события клавиши. Возвращает <c>true</c>, если событие нужно
/// проглотить — тогда приложение переднего плана его не увидит.
/// A key event handler. Returns <c>true</c> when the event must be swallowed —
/// then the foreground application will not see it.
/// </summary>
internal delegate bool KeyFilter(int virtualKey, bool isKeyDown);
@@ -30,14 +30,14 @@ internal sealed class LowLevelKeyboardHook : IDisposable
private const int WmSysKeyDown = 0x0104;
private const int WmSysKeyUp = 0x0105;
/// <summary>Событие пришло не от живого нажатия, а из SendInput.</summary>
/// <summary>The event came from SendInput rather than from a real key press.</summary>
private const uint LowLevelKeyHookFlagInjected = 0x10;
private readonly KeyFilter _filter;
// Делегат живёт в поле не для удобства: ссылку на него держит только Win32,
// о которой сборщик мусора не знает, и без поля хук перестаёт работать
// через случайное время
// The delegate lives in a field not for convenience: the only reference to it is
// held by Win32, which the garbage collector knows nothing about, and without the
// field the hook stops working after a random amount of time
private readonly HookProc _callback;
private IntPtr _handle;
@@ -53,7 +53,7 @@ internal sealed class LowLevelKeyboardHook : IDisposable
internal bool IsInstalled => _handle != IntPtr.Zero;
/// <summary>
/// Ставит хук. Возвращает <c>false</c>, если система отказала.
/// Installs the hook. Returns <c>false</c> when the system refuses.
/// </summary>
internal bool Install()
{
@@ -104,14 +104,14 @@ internal sealed class LowLevelKeyboardHook : IDisposable
var data = Marshal.PtrToStructure<KeyboardHookData>(lParam);
// Синтетический ввод присылают экранные клавиатуры, программы
// автозамены и средства автоматизации: подменять их работу не наше дело
// Synthetic input comes from on-screen keyboards, text expanders and
// automation tools: overriding what they do is none of our business
bool injected = (data.flags & LowLevelKeyHookFlagInjected) != 0;
if ((isKeyDown || isKeyUp) && !injected && _filter((int)data.vkCode, isKeyDown))
{
// Ненулевой результат вместо CallNextHookEx обрывает цепочку: событие
// не дойдёт ни до приложения, ни до обработчика регистра в Windows
// A non-zero result instead of CallNextHookEx breaks the chain: the event
// will reach neither the application nor the Windows caps-lock handler
return 1;
}
+19 -18
View File
@@ -3,8 +3,8 @@ using System.Runtime.InteropServices;
namespace CursorLang.Interop;
/// <summary>
/// Win32 API для окна-подсказки: стили, позиционирование у курсора
/// и масштаб монитора, на котором курсор находится.
/// Win32 API for the popup window: styles, positioning near the cursor
/// and the scale of the monitor the cursor is on.
/// </summary>
internal static class PopupWindowNative
{
@@ -62,9 +62,9 @@ internal static class PopupWindowNative
}
private const int GWL_EXSTYLE = -20;
// Окно не забирает фокус у активного приложения
// The window does not take focus away from the active application
private const int WS_EX_NOACTIVATE = 0x08000000;
// И не попадает в Alt+Tab
// And does not show up in Alt+Tab
private const int WS_EX_TOOLWINDOW = 0x00000080;
private const uint SWP_NOSIZE = 0x0001;
@@ -81,8 +81,8 @@ internal static class PopupWindowNative
}
/// <summary>
/// Подсказка всплывает поверх чужих приложений, поэтому она не должна
/// ни активироваться сама, ни отбирать фокус ввода у активного окна.
/// The popup shows up on top of other applications, so it must neither
/// activate itself nor steal input focus from the active window.
/// </summary>
internal static void MakePassive(IntPtr hWnd)
{
@@ -91,10 +91,10 @@ internal static class PopupWindowNative
}
/// <summary>
/// Двигает окно в точку экрана, не меняя размер и порядок окон.
/// Координаты — физические пиксели: у мониторов разный масштаб, а
/// Window.Left/Top пересчитываются по DPI того монитора, где окно сейчас,
/// и на соседнем мониторе дают промах.
/// Moves the window to a screen point without changing its size or z-order.
/// The coordinates are physical pixels: monitors have different scaling, while
/// Window.Left/Top are converted using the DPI of the monitor the window is on
/// right now, which misses the target on a neighbouring monitor.
/// </summary>
internal static void MoveTo(IntPtr hWnd, int x, int y)
{
@@ -102,26 +102,27 @@ internal static class PopupWindowNative
}
/// <summary>
/// Задаёт положение и размер окна в физических пикселях.
/// Sets the window position and size in physical pixels.
/// </summary>
/// <remarks>
/// Размер выставляется именно так, а не через Width/Height: при первом показе
/// окно ещё подчиняется системному минимальному размеру окна (SM_CXMIN×SM_CYMIN)
/// и подсказка выходит заметно крупнее текста. К моменту вызова окно уже
/// показано и стало popup-окном, на которое это ограничение не действует.
/// The size is set this way rather than through Width/Height: on the first show the
/// window is still subject to the system minimum window size (SM_CXMIN×SM_CYMIN)
/// and the popup comes out noticeably larger than its text. By the time this is
/// called the window is already shown and has become a popup window, which that
/// restriction does not apply to.
/// </remarks>
internal static void SetBounds(IntPtr hWnd, int x, int y, int width, int height)
{
SetWindowPos(hWnd, IntPtr.Zero, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE);
}
/// <summary>Масштаб монитора, на котором находится точка (1.0 при 96 DPI).</summary>
/// <summary>The scale of the monitor the point is on (1.0 at 96 DPI).</summary>
internal static double GetScaleAt(Point point) =>
GetScaleOf(MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST));
/// <summary>
/// Рабочая область монитора с активным окном — без панели задач — и его масштаб.
/// Именно на этом мониторе пользователь сейчас работает.
/// The work area of the monitor holding the active window — without the taskbar —
/// and its scale. That is the monitor the user is working on right now.
/// </summary>
internal static (Rect WorkArea, double Scale) GetActiveMonitorWorkArea()
{
+9 -9
View File
@@ -3,14 +3,14 @@ using System.Runtime.InteropServices;
namespace CursorLang.Interop;
/// <summary>
/// Win32 API для размещения окна настроек: его собственные границы и рабочая
/// область монитора, на который окно просят поставить.
/// Win32 API for placing the settings window: its own bounds and the work area
/// of the monitor the window is asked to be put on.
/// </summary>
/// <remarks>
/// Границы берутся у системы, а не из <c>Window.Left/Top/Width/Height</c>:
/// высота окна подстраивается под содержимое, и WPF пересчитывает эти свойства
/// по DPI монитора, а рабочая область монитора приходит в пикселях. Считать
/// центр в одних единицах проще, чем переводить туда-обратно.
/// The bounds are taken from the system rather than from <c>Window.Left/Top/Width/Height</c>:
/// the window height adapts to its content, and WPF converts those properties using the
/// monitor DPI, while the monitor work area comes in pixels. Computing the centre in a
/// single unit is simpler than converting back and forth.
/// </remarks>
internal static class WindowPlacementNative
{
@@ -34,13 +34,13 @@ internal static class WindowPlacementNative
private const uint MONITOR_DEFAULTTONEAREST = 2;
/// <summary>Границы окна в пикселях экрана — вместе с рамкой и заголовком.</summary>
/// <summary>The window bounds in screen pixels — including the frame and the title bar.</summary>
internal static PopupWindowNative.Rect? TryGetBounds(IntPtr hWnd) =>
GetWindowRect(hWnd, out PopupWindowNative.Rect bounds) ? bounds : null;
/// <summary>
/// Рабочая область — без панели задач — того монитора, на котором
/// прямоугольник находится целиком или хотя бы большей частью.
/// The work area — without the taskbar — of the monitor that holds the
/// rectangle entirely, or at least most of it.
/// </summary>
internal static PopupWindowNative.Rect? TryGetWorkAreaNear(PopupWindowNative.Rect rect)
{
+4 -4
View File
@@ -3,8 +3,8 @@ using System.Runtime.InteropServices;
namespace CursorLang.Interop;
/// <summary>
/// Оформление рамки окна средствами системы: заголовок рисует Windows,
/// и в тёмной теме его нужно переключать отдельно от содержимого окна.
/// Window frame styling by the system: the title bar is drawn by Windows,
/// and in the dark theme it has to be switched separately from the window content.
/// </summary>
internal static class WindowThemeNative
{
@@ -14,8 +14,8 @@ internal static class WindowThemeNative
private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
/// <summary>
/// Перекрашивает заголовок окна. На сборках Windows 10 до 2004 атрибут
/// не поддерживается — заголовок просто останется светлым.
/// Recolours the window title bar. On Windows 10 builds before 2004 the attribute
/// is not supported — the title bar simply stays light.
/// </summary>
internal static void SetDarkTitleBar(IntPtr hWnd, bool isDark)
{