diff --git a/CursorLang/Interop/CaretNative.cs b/CursorLang/Interop/CaretNative.cs index 5310d5d..d2188fa 100644 --- a/CursorLang/Interop/CaretNative.cs +++ b/CursorLang/Interop/CaretNative.cs @@ -6,12 +6,12 @@ using Accessibility; namespace CursorLang.Interop; /// -/// Определяет положение каретки в активном поле ввода — в том числе в чужом приложении. +/// Locates the caret in the active input field — including one in another application. /// /// -/// Единого способа нет: классические 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. /// internal static class CaretNative { @@ -32,8 +32,8 @@ internal static class CaretNative private const int CHILDID_SELF = 0; /// - /// Прямоугольник каретки в пикселях экрана или null, если активное - /// приложение не сообщает её положение. + /// The caret rectangle in screen pixels, or null when the active + /// application does not report its position. /// internal static PopupWindowNative.Rect? TryGetCaretRect() { @@ -50,13 +50,13 @@ internal static class CaretNative } /// - /// Отсеивает заведомо неверные координаты. + /// Filters out obviously wrong coordinates. /// /// - /// Часть приложений отдаёт положение каретки в своей системе координат либо - /// без учёта масштаба экрана, и подсказка уезжает далеко от поля ввода. - /// Каретка обязана находиться внутри окна ввода — это и проверяем, а перед - /// отказом пробуем истолковать координаты как немасштабированные. + /// 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. /// 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; - /// Сколько ждём ответа от чужого приложения по UI Automation. + /// How long we wait for another application to answer over UI Automation. 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 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; } diff --git a/CursorLang/Interop/ForegroundInputNative.cs b/CursorLang/Interop/ForegroundInputNative.cs index 574a518..002ac64 100644 --- a/CursorLang/Interop/ForegroundInputNative.cs +++ b/CursorLang/Interop/ForegroundInputNative.cs @@ -3,8 +3,8 @@ using System.Runtime.InteropServices; namespace CursorLang.Interop; /// -/// Сведения о вводе в активном приложении: какое окно держит фокус клавиатуры -/// и где находится каретка. +/// Input details of the active application: which window holds keyboard focus +/// and where the caret is. /// internal static class ForegroundInputNative { @@ -26,12 +26,12 @@ internal static class ForegroundInputNative private static extern bool GetGUIThreadInfo(uint idThread, ref GuiThreadInfo lpgui); /// - /// Состояние ввода потока переднего плана. + /// The input state of the foreground thread. /// /// - /// Нулевой идентификатор потока не случаен: у современных приложений окно - /// верхнего уровня и окно ввода живут в разных потоках, и спрашивать нужно - /// именно про передний план целиком. + /// 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. /// internal static bool TryGetInfo(out GuiThreadInfo info) { diff --git a/CursorLang/Interop/ForegroundPermissionNative.cs b/CursorLang/Interop/ForegroundPermissionNative.cs index a3a33b7..ff169eb 100644 --- a/CursorLang/Interop/ForegroundPermissionNative.cs +++ b/CursorLang/Interop/ForegroundPermissionNative.cs @@ -3,21 +3,21 @@ using System.Runtime.InteropServices; namespace CursorLang.Interop; /// -/// Право выводить окно на передний план. +/// The right to bring a window to the foreground. /// internal static class ForegroundPermissionNative { - /// ASFW_ANY — право получает любой процесс. + /// ASFW_ANY — any process gets the right. private const uint AnyProcess = 0xFFFFFFFF; /// - /// Уступает своё право вывести окно на передний план другим процессам. + /// Gives up our right to bring a window to the foreground in favour of other processes. /// /// - /// Поменять окно переднего плана 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. /// internal static void GrantToAnyProcess() => AllowSetForegroundWindow(AnyProcess); diff --git a/CursorLang/Interop/KeyboardLayoutNative.cs b/CursorLang/Interop/KeyboardLayoutNative.cs index cd90b32..d425887 100644 --- a/CursorLang/Interop/KeyboardLayoutNative.cs +++ b/CursorLang/Interop/KeyboardLayoutNative.cs @@ -3,16 +3,16 @@ using System.Runtime.InteropServices; namespace CursorLang.Interop; /// -/// Win32 API для определения раскладки активного приложения. +/// Win32 API for reading the layout of the active application. /// internal static class KeyboardLayoutNative { private const uint WmInputLangChangeRequest = 0x0050; - /// Взять следующую раскладку из системного списка. + /// Take the next layout from the system list. private static readonly IntPtr InputLangChangeForward = new(0x0002); - /// HKL_NEXT — та же просьба на языке старых версий Windows. + /// HKL_NEXT — the same request in the language of older Windows versions. 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); /// - /// Раскладка, которой сейчас печатает пользователь. + /// The layout the user is currently typing with. /// internal static int GetActiveLocaleId() => GetLocaleIdOf(GetInputWindow()); /// - /// Просит активное приложение перейти на следующую раскладку из системного списка. + /// Asks the active application to switch to the next layout from the system list. /// /// - /// Синтезировать системное сочетание вроде 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. /// 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); } /// - /// Окно, которому принадлежит ввод с клавиатуры. + /// The window that owns keyboard input. /// /// - /// Спрашиваем окно с фокусом клавиатуры, а не окно верхнего плана: у Блокнота - /// 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. /// private static IntPtr GetInputWindow() { @@ -74,8 +74,8 @@ internal static class KeyboardLayoutNative } /// - /// Идентификатор локали для окна. Раскладка в 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. /// internal static int GetLocaleIdOf(IntPtr hWnd) { diff --git a/CursorLang/Interop/LowLevelKeyboardHook.cs b/CursorLang/Interop/LowLevelKeyboardHook.cs index 1fc3ae8..b0dc779 100644 --- a/CursorLang/Interop/LowLevelKeyboardHook.cs +++ b/CursorLang/Interop/LowLevelKeyboardHook.cs @@ -3,22 +3,22 @@ using System.Runtime.InteropServices; namespace CursorLang.Interop; /// -/// Системный перехватчик клавиатуры (WH_KEYBOARD_LL): видит нажатия во всех -/// приложениях и умеет не пропускать их дальше. +/// A system keyboard hook (WH_KEYBOARD_LL): sees key presses in every application +/// and can keep them from going any further. /// /// -/// Ставится только низкоуровневый хук: обычный 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. /// internal sealed class LowLevelKeyboardHook : IDisposable { /// - /// Обработчик события клавиши. Возвращает true, если событие нужно - /// проглотить — тогда приложение переднего плана его не увидит. + /// A key event handler. Returns true when the event must be swallowed — + /// then the foreground application will not see it. /// 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; - /// Событие пришло не от живого нажатия, а из SendInput. + /// The event came from SendInput rather than from a real key press. 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; /// - /// Ставит хук. Возвращает false, если система отказала. + /// Installs the hook. Returns false when the system refuses. /// internal bool Install() { @@ -104,14 +104,14 @@ internal sealed class LowLevelKeyboardHook : IDisposable var data = Marshal.PtrToStructure(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; } diff --git a/CursorLang/Interop/PopupWindowNative.cs b/CursorLang/Interop/PopupWindowNative.cs index 955b350..8d22efb 100644 --- a/CursorLang/Interop/PopupWindowNative.cs +++ b/CursorLang/Interop/PopupWindowNative.cs @@ -3,8 +3,8 @@ using System.Runtime.InteropServices; namespace CursorLang.Interop; /// -/// Win32 API для окна-подсказки: стили, позиционирование у курсора -/// и масштаб монитора, на котором курсор находится. +/// Win32 API for the popup window: styles, positioning near the cursor +/// and the scale of the monitor the cursor is on. /// 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 } /// - /// Подсказка всплывает поверх чужих приложений, поэтому она не должна - /// ни активироваться сама, ни отбирать фокус ввода у активного окна. + /// The popup shows up on top of other applications, so it must neither + /// activate itself nor steal input focus from the active window. /// internal static void MakePassive(IntPtr hWnd) { @@ -91,10 +91,10 @@ internal static class PopupWindowNative } /// - /// Двигает окно в точку экрана, не меняя размер и порядок окон. - /// Координаты — физические пиксели: у мониторов разный масштаб, а - /// 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. /// internal static void MoveTo(IntPtr hWnd, int x, int y) { @@ -102,26 +102,27 @@ internal static class PopupWindowNative } /// - /// Задаёт положение и размер окна в физических пикселях. + /// Sets the window position and size in physical pixels. /// /// - /// Размер выставляется именно так, а не через 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. /// 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); } - /// Масштаб монитора, на котором находится точка (1.0 при 96 DPI). + /// The scale of the monitor the point is on (1.0 at 96 DPI). internal static double GetScaleAt(Point point) => GetScaleOf(MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST)); /// - /// Рабочая область монитора с активным окном — без панели задач — и его масштаб. - /// Именно на этом мониторе пользователь сейчас работает. + /// 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. /// internal static (Rect WorkArea, double Scale) GetActiveMonitorWorkArea() { diff --git a/CursorLang/Interop/WindowPlacementNative.cs b/CursorLang/Interop/WindowPlacementNative.cs index 954b751..d4fff30 100644 --- a/CursorLang/Interop/WindowPlacementNative.cs +++ b/CursorLang/Interop/WindowPlacementNative.cs @@ -3,14 +3,14 @@ using System.Runtime.InteropServices; namespace CursorLang.Interop; /// -/// 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. /// /// -/// Границы берутся у системы, а не из Window.Left/Top/Width/Height: -/// высота окна подстраивается под содержимое, и WPF пересчитывает эти свойства -/// по DPI монитора, а рабочая область монитора приходит в пикселях. Считать -/// центр в одних единицах проще, чем переводить туда-обратно. +/// The bounds are taken from the system rather than from Window.Left/Top/Width/Height: +/// 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. /// internal static class WindowPlacementNative { @@ -34,13 +34,13 @@ internal static class WindowPlacementNative private const uint MONITOR_DEFAULTTONEAREST = 2; - /// Границы окна в пикселях экрана — вместе с рамкой и заголовком. + /// The window bounds in screen pixels — including the frame and the title bar. internal static PopupWindowNative.Rect? TryGetBounds(IntPtr hWnd) => GetWindowRect(hWnd, out PopupWindowNative.Rect bounds) ? bounds : null; /// - /// Рабочая область — без панели задач — того монитора, на котором - /// прямоугольник находится целиком или хотя бы большей частью. + /// The work area — without the taskbar — of the monitor that holds the + /// rectangle entirely, or at least most of it. /// internal static PopupWindowNative.Rect? TryGetWorkAreaNear(PopupWindowNative.Rect rect) { diff --git a/CursorLang/Interop/WindowThemeNative.cs b/CursorLang/Interop/WindowThemeNative.cs index ff457fd..2ed1f71 100644 --- a/CursorLang/Interop/WindowThemeNative.cs +++ b/CursorLang/Interop/WindowThemeNative.cs @@ -3,8 +3,8 @@ using System.Runtime.InteropServices; namespace CursorLang.Interop; /// -/// Оформление рамки окна средствами системы: заголовок рисует 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. /// internal static class WindowThemeNative { @@ -14,8 +14,8 @@ internal static class WindowThemeNative private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20; /// - /// Перекрашивает заголовок окна. На сборках 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. /// internal static void SetDarkTitleBar(IntPtr hWnd, bool isDark) { diff --git a/CursorLang/Models/AppSettings.cs b/CursorLang/Models/AppSettings.cs index 94f8bb4..03a1031 100644 --- a/CursorLang/Models/AppSettings.cs +++ b/CursorLang/Models/AppSettings.cs @@ -5,67 +5,68 @@ using CommunityToolkit.Mvvm.ComponentModel; namespace CursorLang.Models; /// -/// Настройки приложения. Все изменения применяются на лету: подсказка и окно -/// настроек привязаны к этим свойствам, а SettingsService сохраняет их на диск. +/// The application settings. Every change applies on the fly: the popup and the +/// settings window are bound to these properties, and SettingsService saves +/// them to disk. /// public sealed partial class AppSettings : ObservableObject { - /// Язык интерфейса в виде кода культуры: «ru», «en». + /// The interface language as a culture code: "ru", "en". [ObservableProperty] private string _language = "en"; - /// Оформление окна настроек. + /// The look of the settings window. [ObservableProperty] private AppTheme _theme = AppTheme.System; [ObservableProperty] private PopupPlacementMode _placementMode = PopupPlacementMode.AtCursor; - // Сторона и отступ хранятся отдельно для каждого режима: у курсора и у - // каретки удобны разные настройки, и переключение режима их не сбрасывает + // The side and the offset are stored per mode: the cursor and the caret call for + // different settings, and switching the mode does not reset them [ObservableProperty] private AnchorSide _cursorSide = AnchorSide.BottomRight; - /// Отступ от курсора в единицах WPF. + /// The offset from the cursor in WPF units. [ObservableProperty] private double _cursorOffset = 16; [ObservableProperty] private AnchorSide _caretSide = AnchorSide.BottomRight; - /// Отступ от каретки в единицах WPF. + /// The offset from the caret in WPF units. [ObservableProperty] private double _caretOffset = 16; [ObservableProperty] private ScreenPosition _screenPosition = ScreenPosition.BottomRight; - /// Отступ от края монитора в единицах WPF. + /// The offset from the monitor edge in WPF units. [ObservableProperty] private double _screenMargin = 24; [ObservableProperty] private double _fontSize = 20; - /// Непрозрачность подсказки: 1.0 — полностью непрозрачная. + /// The popup opacity: 1.0 is fully opaque. [ObservableProperty] private double _opacity = 0.9; - /// Сколько подсказка держится на экране, в миллисекундах. + /// How long the popup stays on screen, in milliseconds. [ObservableProperty] private double _durationMilliseconds = 500; /// - /// Перехватывать Caps Lock и переключать по ней раскладку вместо смены регистра. - /// По умолчанию выключено: приложение не должно менять поведение системы, - /// пока его об этом не попросили. + /// Intercept Caps Lock and switch the layout with it instead of changing the case. + /// Off by default: the application must not change the behaviour of the system + /// until it is asked to. /// [ObservableProperty] private bool _useCapsLockHotkey; /// - /// С какого времени удержания Caps Lock переключение отменяется, в миллисекундах. + /// After how long a Caps Lock hold cancels the switch, in milliseconds. /// [ObservableProperty] private double _capsLockHoldMilliseconds = 300; diff --git a/CursorLang/Models/AppTheme.cs b/CursorLang/Models/AppTheme.cs index 6710f6d..0988653 100644 --- a/CursorLang/Models/AppTheme.cs +++ b/CursorLang/Models/AppTheme.cs @@ -1,8 +1,8 @@ namespace CursorLang.Models; /// -/// Оформление окна настроек. По умолчанию приложение следует теме Windows, -/// но пользователь может закрепить светлую или тёмную. +/// The look of the settings window. By default the application follows the Windows +/// theme, but the user can pin the light or the dark one. /// public enum AppTheme { diff --git a/CursorLang/Models/KeyboardLayout.cs b/CursorLang/Models/KeyboardLayout.cs index e114260..a778d1f 100644 --- a/CursorLang/Models/KeyboardLayout.cs +++ b/CursorLang/Models/KeyboardLayout.cs @@ -3,16 +3,16 @@ using System.Globalization; namespace CursorLang.Models; /// -/// Раскладка клавиатуры в удобном для отображения виде. +/// A keyboard layout in a form convenient for display. /// -/// Идентификатор локали (младшее слово HKL). -/// Короткое имя для подсказки у курсора, например «RU». -/// Полное имя, например «RU — русский (Россия)». +/// The locale identifier (the low word of HKL). +/// A short name for the popup at the cursor, "RU" for instance. +/// The full name, "RU — русский (Россия)" for instance. public sealed record KeyboardLayout(int LocaleId, string ShortName, string DisplayName) { /// - /// Строит модель по идентификатору локали. Неизвестные локали не являются - /// ошибкой: для них показываем сам идентификатор. + /// Builds the model from a locale identifier. Unknown locales are not an + /// error: for them we show the identifier itself. /// public static KeyboardLayout FromLocaleId(int localeId) { diff --git a/CursorLang/Models/LayoutChangedEventArgs.cs b/CursorLang/Models/LayoutChangedEventArgs.cs index cbe08a2..545f304 100644 --- a/CursorLang/Models/LayoutChangedEventArgs.cs +++ b/CursorLang/Models/LayoutChangedEventArgs.cs @@ -1,19 +1,19 @@ namespace CursorLang.Models; /// -/// Почему изменилась текущая раскладка. +/// Why the current layout has changed. /// public enum LayoutChangeReason { - /// Пользователь переключил раскладку в активном приложении. + /// The user switched the layout in the active application. UserSwitched, - /// Пользователь перешёл в другое приложение, где своя раскладка. + /// The user moved to another application that has a layout of its own. ApplicationSwitched, } /// -/// Данные события смены раскладки. +/// The data of a layout change event. /// public sealed class LayoutChangedEventArgs(KeyboardLayout layout, LayoutChangeReason reason) : EventArgs { diff --git a/CursorLang/Models/PopupPlacement.cs b/CursorLang/Models/PopupPlacement.cs index 826a7b9..f9afdd8 100644 --- a/CursorLang/Models/PopupPlacement.cs +++ b/CursorLang/Models/PopupPlacement.cs @@ -1,25 +1,25 @@ namespace CursorLang.Models; /// -/// Способ выбора места для подсказки. +/// How the place for the popup is chosen. /// public enum PopupPlacementMode { - /// Рядом с курсором мыши. + /// Next to the mouse cursor. AtCursor, /// - /// Рядом с кареткой в активном поле ввода. Если приложение не сообщает - /// её положение, подсказка показывается у курсора мыши. + /// Next to the caret in the active input field. When the application does not + /// report its position, the popup is shown at the mouse cursor. /// AtCaret, - /// В заданной точке монитора с активным окном. + /// At a fixed point of the monitor holding the active window. FixedPoint, } /// -/// С какой стороны от курсора или каретки показывать подсказку. +/// Which side of the cursor or the caret to show the popup on. /// public enum AnchorSide { @@ -32,7 +32,7 @@ public enum AnchorSide } /// -/// Место на мониторе для режима . +/// The place on the monitor for the mode. /// public enum ScreenPosition { diff --git a/CursorLang/Services/CapsLockHotkeyService.cs b/CursorLang/Services/CapsLockHotkeyService.cs index 8a1a9ed..f0748fc 100644 --- a/CursorLang/Services/CapsLockHotkeyService.cs +++ b/CursorLang/Services/CapsLockHotkeyService.cs @@ -5,13 +5,13 @@ using CursorLang.Models; namespace CursorLang.Services; /// -/// Держит системный перехват Caps Lock и отличает короткое нажатие от удержания. +/// Holds the system Caps Lock hook and tells a short tap from a hold. /// /// -/// Различить их можно только по факту отпускания клавиши, поэтому перехватываются -/// оба события — и нажатие, и отпускание. Заодно это единственный способ отменить -/// смену регистра: Windows переключает Caps Lock по событию нажатия, и пропустить -/// его «на всякий случай» нельзя. +/// They can be told apart only by the key being released, so both events are +/// intercepted — the press and the release. That is also the only way to cancel the +/// case change: Windows toggles Caps Lock on the press event, and letting it through +/// "just in case" is not an option. /// public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable { @@ -28,7 +28,7 @@ public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable public CapsLockHotkeyService(AppSettings settings) { _settings = settings; - _hook = new LowLevelKeyboardHook(OnKeyEvent); + _hook = new LowLevelKeyboardHook(HandleKeyEvent); _holdTimer.Tick += OnHoldTimerTick; } @@ -48,9 +48,9 @@ public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable ResetPress(); } - // При закрытии приложения событий уже никто не ждёт, поэтому в отличие - // от Stop состояние сбрасывается молча: очередь диспетчера в этот момент - // может быть закрыта + // When the application is closing, nobody is waiting for events any more, so + // unlike in Stop the state is reset quietly: the dispatcher queue may be shut + // down by that moment public void Dispose() { _holdTimer.Tick -= OnHoldTimerTick; @@ -60,10 +60,12 @@ public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable _hook.Dispose(); } - // Вызывается системным хуком, то есть внутри разбора очереди сообщений. - // Здесь только учёт состояния: показывать окна и рассылать события отсюда - // нельзя — обработчик обязан вернуть управление за считанные миллисекунды - private bool OnKeyEvent(int virtualKey, bool isKeyDown) + // Called by the system hook, that is, inside message queue processing. Only state + // tracking belongs here: showing windows and raising events from here is not + // allowed — the handler must return control within a few milliseconds. + // In tests the key presses are fed here as well: there is no need to install a + // real keyboard hook just to check how presses are interpreted + internal bool HandleKeyEvent(int virtualKey, bool isKeyDown) { if (virtualKey != VirtualKeyCapsLock) { @@ -72,13 +74,13 @@ public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable if (isKeyDown) { - // Пока клавишу держат, Windows повторяет нажатие: отсчёт удержания - // ведём от первого события и на повторы не реагируем + // While the key is held, Windows repeats the press: the hold is counted + // from the first event and the repeats are ignored if (!_isPressed) { _isPressed = true; - // Порог читаем при каждом нажатии: его меняют в настройках на лету + // The threshold is read on every press: it is changed in the settings on the fly _holdTimer.Interval = _settings.CapsLockHoldDelay; _holdTimer.Start(); } @@ -109,8 +111,8 @@ public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable HoldStarted?.Invoke(this, EventArgs.Empty); } - // Событие уходит подписчикам после возврата из хука: они вправе показывать - // окна и делать что угодно ещё, не задерживая обработку нажатия + // The event reaches the subscribers after the hook returns: they are free to show + // windows and do anything else without holding up the handling of the key press private void Notify(EventHandler? handler) { if (handler is not null) @@ -119,8 +121,8 @@ public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable } } - // Перехват могли снять с зажатой клавишей — например, сняв галочку - // в настройках. Подсказку в этом случае нужно убрать + // The hook may have been removed with the key held down — by clearing the + // checkbox in the settings, for instance. The popup has to be taken down then private void ResetPress() { _isPressed = false; diff --git a/CursorLang/Services/CapsLockSwitchCoordinator.cs b/CursorLang/Services/CapsLockSwitchCoordinator.cs index a7b7a50..10af38c 100644 --- a/CursorLang/Services/CapsLockSwitchCoordinator.cs +++ b/CursorLang/Services/CapsLockSwitchCoordinator.cs @@ -4,9 +4,9 @@ using CursorLang.Models; namespace CursorLang.Services; /// -/// Превращает нажатия Caps Lock в действия: короткое — переключает раскладку, -/// длинное — только показывает подсказку. Он же включает и выключает перехват -/// вслед за галочкой в настройках. +/// Turns Caps Lock presses into actions: a short one switches the layout, a long one +/// only shows the popup. It also turns the hook on and off following the checkbox in +/// the settings. /// public sealed class CapsLockSwitchCoordinator : IDisposable { @@ -68,8 +68,8 @@ public sealed class CapsLockSwitchCoordinator : IDisposable private void OnTapped(object? sender, EventArgs e) => _layoutService.SwitchToNext(); - // Раскладку не меняем, но и молчать нельзя: без подсказки долгое нажатие - // выглядит как то, что клавиша просто не сработала + // We do not change the layout, but staying silent will not do either: without the + // popup a long press looks as if the key simply did not work private void OnHoldStarted(object? sender, EventArgs e) => _popupService.ShowUntilHidden(_layoutService.Current); diff --git a/CursorLang/Services/ICapsLockHotkeyService.cs b/CursorLang/Services/ICapsLockHotkeyService.cs index c0fd0e6..32f0152 100644 --- a/CursorLang/Services/ICapsLockHotkeyService.cs +++ b/CursorLang/Services/ICapsLockHotkeyService.cs @@ -1,29 +1,29 @@ namespace CursorLang.Services; /// -/// Перехватывает Caps Lock на уровне системы и делит нажатия на короткие -/// и длинные. Что делать с ними — решают подписчики. +/// Intercepts Caps Lock at the system level and splits the presses into short and +/// long ones. What to do with them is up to the subscribers. /// public interface ICapsLockHotkeyService { - /// Короткое нажатие: клавишу отпустили до порога удержания. + /// A short press: the key was released before the hold threshold. event EventHandler? Tapped; - /// Порог удержания пройден, клавишу всё ещё держат. + /// The hold threshold has passed, the key is still held. event EventHandler? HoldStarted; - /// Удержание закончилось: клавишу отпустили. + /// The hold is over: the key was released. event EventHandler? HoldEnded; - /// Стоит ли перехват прямо сейчас. + /// Whether the hook is installed right now. bool IsRunning { get; } /// - /// Начинает перехват. Вызывать нужно из потока пользовательского интерфейса: - /// системный хук клавиатуры работает только на потоке с циклом сообщений. + /// Starts intercepting. Must be called from the user interface thread: + /// a system keyboard hook works only on a thread with a message loop. /// void Start(); - /// Снимает перехват, возвращая клавише её обычное поведение. + /// Removes the hook, giving the key its usual behaviour back. void Stop(); } diff --git a/CursorLang/Services/IKeyboardLayoutService.cs b/CursorLang/Services/IKeyboardLayoutService.cs index 3687168..ccb049b 100644 --- a/CursorLang/Services/IKeyboardLayoutService.cs +++ b/CursorLang/Services/IKeyboardLayoutService.cs @@ -3,12 +3,12 @@ using CursorLang.Models; namespace CursorLang.Services; /// -/// Следит за раскладкой активного окна — в том числе в чужих приложениях — -/// и умеет её переключать. +/// Tracks the layout of the active window — including in other applications — +/// and can switch it. /// public interface IKeyboardLayoutService { - /// Раскладка активного окна на текущий момент. + /// The layout of the active window at the moment. KeyboardLayout Current { get; } event EventHandler? LayoutChanged; diff --git a/CursorLang/Services/ILayoutPopupService.cs b/CursorLang/Services/ILayoutPopupService.cs index abea18f..71b1fa0 100644 --- a/CursorLang/Services/ILayoutPopupService.cs +++ b/CursorLang/Services/ILayoutPopupService.cs @@ -3,16 +3,16 @@ using CursorLang.Models; namespace CursorLang.Services; /// -/// Показывает подсказку с раскладкой у курсора. +/// Shows the layout popup at the cursor. /// public interface ILayoutPopupService { - /// Показывает подсказку и убирает её через заданное в настройках время. + /// Shows the popup and takes it down after the time set in the settings. void Show(KeyboardLayout layout); /// - /// Показывает подсказку до явного вызова : нужна там, где - /// время показа задаёт не таймер, а действие пользователя. + /// Shows the popup until is called explicitly: needed where the + /// show time is set by a user action rather than by a timer. /// void ShowUntilHidden(KeyboardLayout layout); diff --git a/CursorLang/Services/ILocalizationService.cs b/CursorLang/Services/ILocalizationService.cs index 6bab8a0..b3497e4 100644 --- a/CursorLang/Services/ILocalizationService.cs +++ b/CursorLang/Services/ILocalizationService.cs @@ -2,21 +2,21 @@ using System.ComponentModel; namespace CursorLang.Services; -/// Язык интерфейса для выбора в настройках. -/// Код культуры: «ru», «en». -/// Название на самом этом языке. +/// An interface language to choose from in the settings. +/// The culture code: "ru", "en". +/// The name in that very language. public sealed record LanguageOption(string Code, string DisplayName) { - // Средства доступности берут имя элемента списка отсюда + // Accessibility tools take the name of the list item from here public override string ToString() => DisplayName; } /// -/// Даёт строки интерфейса и умеет менять язык без перезапуска. +/// Provides the interface strings and can change the language without a restart. /// public interface ILocalizationService : INotifyPropertyChanged { - /// Строка по ключу ресурса. Привязки обновляются при смене языка. + /// The string for a resource key. Bindings update when the language changes. string this[string key] { get; } IReadOnlyList AvailableLanguages { get; } diff --git a/CursorLang/Services/IThemeService.cs b/CursorLang/Services/IThemeService.cs index 4712949..8c1a6d3 100644 --- a/CursorLang/Services/IThemeService.cs +++ b/CursorLang/Services/IThemeService.cs @@ -4,16 +4,16 @@ using CursorLang.Models; namespace CursorLang.Services; /// -/// Применяет светлое или тёмное оформление к окнам приложения. +/// Applies the light or the dark look to the windows of the application. /// public interface IThemeService { - /// Тема, действующая сейчас. + /// The theme in effect right now. AppTheme CurrentTheme { get; } /// - /// Подключает окно к смене темы: заголовок окна рисует Windows, - /// и его цвет приходится переключать для каждого окна отдельно. + /// Hooks a window up to theme changes: the window title bar is drawn by Windows, + /// and its colour has to be switched for each window separately. /// void Register(Window window); } diff --git a/CursorLang/Services/KeyboardLayoutService.cs b/CursorLang/Services/KeyboardLayoutService.cs index 4810e7e..d23015a 100644 --- a/CursorLang/Services/KeyboardLayoutService.cs +++ b/CursorLang/Services/KeyboardLayoutService.cs @@ -5,23 +5,24 @@ using CursorLang.Models; namespace CursorLang.Services; /// -/// Настройки слежения за раскладкой. +/// The settings of layout tracking. /// public sealed class KeyboardLayoutOptions { - /// Как часто проверять раскладку активного окна. + /// How often to check the layout of the active window. public TimeSpan PollInterval { get; init; } = TimeSpan.FromMilliseconds(150); } /// -/// Опрашивает активное окно по таймеру. +/// Polls the active window on a timer. /// /// -/// Опрос выбран не от простоты: событийных способов узнать о смене раскладки -/// в чужом процессе из managed-кода нет. HSHELL_LANGUAGE от RegisterShellHookWindow -/// в Windows 10/11 не приходит, а уведомления TSF (ITfLanguageProfileNotifySink) -/// сообщают только о смене языка внутри своего процесса — оба варианта проверены -/// и не сработали. Один тик — три вызова Win32, читающих данные из памяти ядра. +/// Polling was chosen not for simplicity: there is no event-based way to learn about +/// a layout change in another process from managed code. HSHELL_LANGUAGE from +/// RegisterShellHookWindow does not arrive on Windows 10/11, and TSF notifications +/// (ITfLanguageProfileNotifySink) report only language changes inside our own process +/// — both options were tried and did not work. One tick is three Win32 calls reading +/// data from kernel memory. /// public sealed class KeyboardLayoutService : IKeyboardLayoutService, IDisposable { @@ -75,9 +76,9 @@ public sealed class KeyboardLayoutService : IKeyboardLayoutService, IDisposable _lastLocaleId = localeId; - // Переход в другое приложение со своей раскладкой — не то же самое, - // что переключение раскладки пользователем, и подписчики вправе - // реагировать на эти случаи по-разному + // Moving to another application with a layout of its own is not the same as + // the user switching the layout, and the subscribers are free to react to + // these cases differently LayoutChangeReason reason = appSwitched ? LayoutChangeReason.ApplicationSwitched : LayoutChangeReason.UserSwitched; diff --git a/CursorLang/Services/LayoutNotificationCoordinator.cs b/CursorLang/Services/LayoutNotificationCoordinator.cs index 514dd1e..df6ba3d 100644 --- a/CursorLang/Services/LayoutNotificationCoordinator.cs +++ b/CursorLang/Services/LayoutNotificationCoordinator.cs @@ -3,8 +3,8 @@ using CursorLang.Models; namespace CursorLang.Services; /// -/// Связывает слежение за раскладкой с показом подсказки. -/// Живёт всё время работы приложения независимо от открытых окон. +/// Ties layout tracking to showing the popup. +/// Lives for as long as the application runs, regardless of the open windows. /// public sealed class LayoutNotificationCoordinator : IDisposable { @@ -31,8 +31,8 @@ public sealed class LayoutNotificationCoordinator : IDisposable private void OnLayoutChanged(object? sender, LayoutChangedEventArgs e) { - // При переходе в другое приложение раскладка меняется без участия - // пользователя, и всплывающая подсказка была бы навязчивой + // When moving to another application the layout changes without the user + // taking part, and a popup would be intrusive if (e.Reason == LayoutChangeReason.UserSwitched) { _popupService.Show(e.Layout); diff --git a/CursorLang/Services/LayoutPopupService.cs b/CursorLang/Services/LayoutPopupService.cs index 124d23b..e2deda3 100644 --- a/CursorLang/Services/LayoutPopupService.cs +++ b/CursorLang/Services/LayoutPopupService.cs @@ -6,8 +6,8 @@ using CursorLang.Views; namespace CursorLang.Services; /// -/// Управляет временем жизни подсказки: окно отвечает только за показ, -/// а решение «когда показать и когда убрать» принимается здесь. +/// Manages the lifetime of the popup: the window is only responsible for showing it, +/// while the decision of when to show and when to take it down is made here. /// public sealed class LayoutPopupService : ILayoutPopupService, IDisposable { @@ -29,8 +29,8 @@ public sealed class LayoutPopupService : ILayoutPopupService, IDisposable { ShowUntilHidden(layout); - // Длительность читаем при каждом показе: её меняют в настройках на лету. - // Перезапуск таймера заодно продлевает показ при быстрых переключениях + // The duration is read on every show: it is changed in the settings on the fly. + // Restarting the timer also prolongs the show on quick switches _hideTimer.Interval = _settings.Duration; _hideTimer.Start(); } diff --git a/CursorLang/Services/LocalizationService.cs b/CursorLang/Services/LocalizationService.cs index d7697ba..b4b1575 100644 --- a/CursorLang/Services/LocalizationService.cs +++ b/CursorLang/Services/LocalizationService.cs @@ -6,7 +6,8 @@ using CommunityToolkit.Mvvm.ComponentModel; namespace CursorLang.Services; /// -/// Берёт строки из ресурсов и при смене языка просит WPF перечитать все привязки. +/// Takes the strings from the resources and, when the language changes, asks WPF to +/// re-read every binding. /// public sealed class LocalizationService : ObservableObject, ILocalizationService { @@ -38,8 +39,8 @@ public sealed class LocalizationService : ObservableObject, ILocalizationService OnPropertyChanged(nameof(CurrentLanguage)); - // Сообщаем об изменении индексатора: так обновляются все привязки - // вида {Binding Localization[Key]}, то есть весь текст интерфейса + // We report a change of the indexer: that is how every binding of the + // {Binding Localization[Key]} kind updates, that is, all the interface text OnPropertyChanged(Binding.IndexerName); } } diff --git a/CursorLang/Services/MainWindowPlacement.cs b/CursorLang/Services/MainWindowPlacement.cs index 8ee7d96..c4306c8 100644 --- a/CursorLang/Services/MainWindowPlacement.cs +++ b/CursorLang/Services/MainWindowPlacement.cs @@ -5,25 +5,26 @@ using CursorLang.Interop; namespace CursorLang.Services; /// -/// Решает, где появиться окну настроек: впервые за сеанс — по центру монитора, -/// на котором пользователь работает, а затем — там, где он это окно оставил. +/// Decides where the settings window shows up: for the first time in a session — in +/// the centre of the monitor the user is working on, and after that — where they +/// left that window. /// /// -/// Положение живёт только в памяти и между запусками не сохраняется: набор -/// мониторов к следующему запуску может стать другим, а «по центру активного» -/// верно всегда. +/// The position lives in memory only and is not kept between launches: the set of +/// monitors may be different by the next launch, while "in the centre of the active +/// one" is always right. /// public sealed class MainWindowPlacement { private PopupWindowNative.Point? _position; - // Окно сообщает о переносе и тогда, когда двигаем его мы сами; - // запоминать надо только то, что выбрал пользователь + // The window reports a move when we move it ourselves as well; + // what has to be remembered is only what the user chose private bool _isPlacing; /// - /// Берёт на себя размещение окна: ставит его на место к первому показу - /// и следит за тем, куда пользователь его переносит. + /// Takes over the placement of the window: puts it in place by the first show + /// and follows where the user moves it. /// public void Attach(Window window) { @@ -32,13 +33,13 @@ public sealed class MainWindowPlacement } /// - /// Возвращает окно на запомненное место, а если за этот сеанс его ещё - /// не показывали — ставит по центру активного монитора. + /// Returns the window to the remembered place, and when it has not been shown + /// yet during this session — puts it in the centre of the active monitor. /// public void Apply(Window window) { - // У свёрнутого окна нет осмысленных границ: его разворачивают на прежнем - // месте, и разместить его можно только после этого + // A minimized window has no meaningful bounds: it is restored in its former + // place, and it can be positioned only after that if (window.WindowState != WindowState.Normal) { return; @@ -69,10 +70,10 @@ public sealed class MainWindowPlacement _position = target; } - // Высота окна подстраивается под содержимое, и до первой раскладки она - // неизвестна — по центру встал бы пустой каркас окна. Раскладку поэтому - // просим посчитать сразу: окно к этому моменту ещё не показано, так что - // на прежнем месте оно не мелькнёт + // The window height adapts to its content and is unknown until the first layout + // pass — an empty window frame would end up in the centre. So we ask for the + // layout to be computed right away: by that moment the window is not shown yet, + // so it will not flash in its former place private void OnSourceInitialized(object? sender, EventArgs e) { if (sender is not Window window) @@ -115,15 +116,15 @@ public sealed class MainWindowPlacement } /// - /// Подтягивает окно в рабочую область ближайшего монитора. + /// Pulls the window into the work area of the nearest monitor. /// /// - /// Нужно в двух случаях. Монитор, на который окно поставил пользователь, - /// за сеанс может быть отключён — возвращать окно на его место значило бы - /// оставить пользователя без окна, поэтому запомненное положение здесь - /// пожелание, а не приказ. А высота окна равна высоте содержимого и на - /// невысоком мониторе может рабочую область превысить — тогда у окна, - /// поставленного по центру, заголовок ушёл бы за верхний край. + /// Needed in two cases. The monitor the user put the window on may be disconnected + /// during the session — returning the window to its place would then leave the + /// user without a window, so the remembered position is a wish here rather than an + /// order. And the window height equals the height of its content and may exceed + /// the work area on a short monitor — then the title bar of a window placed in the + /// centre would go past the top edge. /// private static PopupWindowNative.Point? KeepOnScreen( PopupWindowNative.Point position, PopupWindowNative.Rect bounds) diff --git a/CursorLang/Services/SettingsService.cs b/CursorLang/Services/SettingsService.cs index 792077d..1dd0ff0 100644 --- a/CursorLang/Services/SettingsService.cs +++ b/CursorLang/Services/SettingsService.cs @@ -9,8 +9,20 @@ using CursorLang.Models; namespace CursorLang.Services; /// -/// Хранит настройки в %APPDATA%\CursorLang\settings.json. +/// Keeps the settings in the settings.json file. /// +/// +/// The location of the file depends on how the application is installed. A package +/// from the Store keeps its settings in a folder of its own: Windows removes it +/// together with the application, and after the removal nothing superfluous is left +/// in the system — that is what Store applications are expected to do. A separately +/// installed application keeps its settings in %APPDATA%, as before. +/// +/// Settings left over from a separately installed application are picked up by the +/// package on the first launch and moved over. The original file stays where it is: +/// both versions can be installed side by side, and the application has no right to +/// delete settings that are not its own. +/// public sealed class SettingsService : IDisposable { private static readonly JsonSerializerOptions SerializerOptions = new() @@ -37,8 +49,8 @@ public sealed class SettingsService : IDisposable } /// - /// Читает настройки с диска либо отдаёт значения по умолчанию, - /// и дальше сам сохраняет любые изменения. + /// Reads the settings from disk or returns the default values, + /// and from then on saves any changes by itself. /// public AppSettings Load() { @@ -61,7 +73,7 @@ public sealed class SettingsService : IDisposable } catch (Exception e) when (e is IOException or UnauthorizedAccessException) { - // Настройки — не тот случай, ради которого стоит ронять приложение + // The settings are not the kind of thing worth bringing the application down for } } @@ -112,7 +124,7 @@ public sealed class SettingsService : IDisposable Save(); } - // Color не сериализуется штатно, а хранить его читаемым в файле удобно + // Color is not serialized out of the box, and keeping it readable in the file is handy private sealed class ColorJsonConverter : JsonConverter { public override Color Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) diff --git a/CursorLang/Services/SingleInstanceGate.cs b/CursorLang/Services/SingleInstanceGate.cs index 1f11749..bded8a0 100644 --- a/CursorLang/Services/SingleInstanceGate.cs +++ b/CursorLang/Services/SingleInstanceGate.cs @@ -4,14 +4,15 @@ using CursorLang.Interop; namespace CursorLang.Services; /// -/// Пускает работать только один экземпляр приложения: повторный запуск не -/// поднимает второе окно, а показывает окно уже работающего. +/// Lets only one instance of the application run: a second launch does not bring up +/// a second window but shows the window of the one already running. /// /// -/// Имена объектов ядра оставлены без префикса Global, то есть живут в пространстве -/// имён сеанса. Один экземпляр на всю машину дал бы при быстром переключении -/// пользователей странную картину: второй пользователь остался бы без приложения, -/// а показать ему окно первого всё равно нельзя — окна принадлежат сеансу. +/// The kernel object names are left without the Global prefix, that is, they live in +/// the session namespace. A single instance for the whole machine would make for an +/// odd picture with fast user switching: the second user would be left without the +/// application, and showing them the window of the first one is impossible anyway — +/// windows belong to a session. /// public sealed class SingleInstanceGate : IDisposable { @@ -29,8 +30,8 @@ public sealed class SingleInstanceGate : IDisposable public event EventHandler? ActivationRequested; /// - /// Занимает место единственного экземпляра. Если приложение уже работает, - /// просит его показаться и возвращает false — вызвавшему остаётся выйти. + /// Takes the single-instance slot. When the application is already running, asks + /// it to show itself and returns false — the caller is left to exit. /// public bool TryAcquire() { @@ -42,15 +43,15 @@ public sealed class SingleInstanceGate : IDisposable } catch (AbandonedMutexException) { - // Предыдущий экземпляр завершился аварийно и мьютекс не отпустил. - // Владельца у него теперь нет, а значит место свободно + // The previous instance crashed and did not release the mutex. + // It has no owner now, which means the slot is free _isOwner = true; } - // Событие открывают оба экземпляра: первый — чтобы ждать просьбы, - // второй — чтобы её подать. Кто из них создаст объект, зависит от того, - // кто оказался первым, и на работу не влияет - _activationRequest = new EventWaitHandle(false, EventResetMode.AutoReset, ActivationEventName); + // The event is opened by both instances: the first one to wait for a request, + // the second one to make it. Which of them creates the object depends on who + // came first and does not affect the work + _activationRequest = new EventWaitHandle(false, EventResetMode.AutoReset, _activationEventName); if (!_isOwner) { @@ -59,8 +60,8 @@ public sealed class SingleInstanceGate : IDisposable return false; } - // Ожидание отдано пулу потоков: держать ради него свой поток не за чем, - // а просьба может не прийти никогда + // The wait is handed over to the thread pool: there is no reason to hold a + // thread of our own for it, and the request may never come _activationWait = ThreadPool.RegisterWaitForSingleObject( _activationRequest, OnActivationSignalled, @@ -79,8 +80,8 @@ public sealed class SingleInstanceGate : IDisposable _activationRequest?.Dispose(); _activationRequest = null; - // Мьютекс отпускает тот же поток, что его занял: и то и другое - // происходит на потоке пользовательского интерфейса + // The mutex is released by the same thread that took it: both happen + // on the user interface thread if (_isOwner) { _mutex?.ReleaseMutex(); @@ -91,8 +92,8 @@ public sealed class SingleInstanceGate : IDisposable _mutex = null; } - // Пул потоков сообщает о просьбе где придётся, а окно слушается только - // своего потока + // The thread pool reports the request from wherever it happens to be, while the + // window obeys only its own thread private void OnActivationSignalled(object? state, bool timedOut) => _dispatcher.BeginInvoke(() => ActivationRequested?.Invoke(this, EventArgs.Empty)); } diff --git a/CursorLang/Services/ThemeService.cs b/CursorLang/Services/ThemeService.cs index b8339a9..03a27e8 100644 --- a/CursorLang/Services/ThemeService.cs +++ b/CursorLang/Services/ThemeService.cs @@ -8,8 +8,8 @@ using Microsoft.Win32; namespace CursorLang.Services; /// -/// Держит в ресурсах приложения палитру выбранной темы и подменяет её -/// при смене настройки — окна перекрашиваются без перезапуска. +/// Keeps the palette of the chosen theme in the application resources and swaps it +/// when the setting changes — the windows are recoloured without a restart. /// public sealed class ThemeService : IThemeService, IDisposable { @@ -29,7 +29,7 @@ public sealed class ThemeService : IThemeService, IDisposable Apply(); } - /// Тема, которую видит пользователь: System здесь уже разрешён. + /// The theme the user sees: System is already resolved here. public AppTheme CurrentTheme => _current; public void Register(Window window) @@ -60,8 +60,8 @@ public sealed class ThemeService : IThemeService, IDisposable _windows.Clear(); } - /// Тема приложений в настройках Windows. - private static AppTheme DetectSystemTheme() + /// The app theme from the Windows settings. + internal static AppTheme DetectSystemTheme() { try { @@ -84,7 +84,7 @@ public sealed class ThemeService : IThemeService, IDisposable } } - // Windows сообщает о смене оформления не из потока интерфейса + // Windows reports a theme change from outside the interface thread private void OnUserPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e) => Application.Current?.Dispatcher.InvokeAsync(Apply); diff --git a/CursorLang/ViewModels/LayoutPopupViewModel.cs b/CursorLang/ViewModels/LayoutPopupViewModel.cs index 990e113..b529d62 100644 --- a/CursorLang/ViewModels/LayoutPopupViewModel.cs +++ b/CursorLang/ViewModels/LayoutPopupViewModel.cs @@ -4,8 +4,8 @@ using CursorLang.Models; namespace CursorLang.ViewModels; /// -/// Содержимое подсказки у курсора. Внешний вид берётся прямо из настроек, -/// поэтому их правка применяется без перезапуска. +/// The content of the popup at the cursor. The look is taken straight from the +/// settings, so editing them applies without a restart. /// public sealed partial class LayoutPopupViewModel : ObservableObject { diff --git a/CursorLang/ViewModels/SettingsViewModel.cs b/CursorLang/ViewModels/SettingsViewModel.cs index c459d6e..70361f7 100644 --- a/CursorLang/ViewModels/SettingsViewModel.cs +++ b/CursorLang/ViewModels/SettingsViewModel.cs @@ -8,9 +8,9 @@ using CursorLang.Services; namespace CursorLang.ViewModels; /// -/// Вариант выбора в списке. Подпись меняется вместе с языком, а сам объект -/// живёт всё время работы окна: если пересоздавать элементы списка, -/// ComboBox сбрасывает выбранное значение. +/// An option in a list. The caption changes together with the language, while the +/// object itself lives for as long as the window does: recreating the list items +/// makes the ComboBox drop the selected value. /// public sealed class EnumOption : ObservableObject where T : struct, Enum { @@ -30,13 +30,13 @@ public sealed class EnumOption : ObservableObject where T : struct, Enum set => SetProperty(ref _display, value); } - // Средства доступности берут имя элемента списка отсюда + // Accessibility tools take the name of the list item from here public override string ToString() => Display; } /// -/// Окно настроек. Значения правятся прямо в , -/// поэтому подсказка подхватывает их сразу, без кнопки «Применить». +/// The settings window. The values are edited right in , +/// so the popup picks them up at once, without an "Apply" button. /// public sealed partial class SettingsViewModel : ObservableObject, IDisposable { @@ -69,7 +69,7 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable Settings = settings; Localization = localization; - // Язык интерфейса — такая же настройка, как остальные, и хранится там же + // The interface language is a setting like any other and is stored in the same place Localization.CurrentLanguage = settings.Language; Settings.PropertyChanged += OnSettingsChanged; Localization.PropertyChanged += OnLocalizationChanged; @@ -110,8 +110,8 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable } } - // Подписи вариантов приходят из ресурсов, поэтому при смене языка - // обновляем только текст — сами элементы списков остаются прежними + // The option captions come from the resources, so on a language change we update + // only the text — the list items themselves stay the same private void OnLocalizationChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName != Binding.IndexerName) @@ -136,7 +136,7 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable } } - // Ключ ресурса собирается из имени типа и значения: PopupPlacementMode_AtCursor + // The resource key is built from the type name and the value: PopupPlacementMode_AtCursor private string GetDisplayName(T value) where T : struct, Enum => Localization[$"{typeof(T).Name}_{value}"]; } diff --git a/CursorLang/Views/Converters.cs b/CursorLang/Views/Converters.cs index f0b5950..ae9ac5b 100644 --- a/CursorLang/Views/Converters.cs +++ b/CursorLang/Views/Converters.cs @@ -6,9 +6,9 @@ using System.Windows.Media; namespace CursorLang.Views; /// -/// Показывает элемент, если значение совпадает с одним из перечисленных -/// в параметре через запятую. Нужен, чтобы настройки точки привязки -/// и настройки экрана не показывались одновременно. +/// Shows an element when the value matches one of those listed in the parameter, +/// separated by commas. Needed so that the anchor point settings and the screen +/// settings are not shown at the same time. /// public sealed class EnumToVisibilityConverter : IValueConverter { @@ -27,8 +27,8 @@ public sealed class EnumToVisibilityConverter : IValueConverter } /// -/// Цвет в запись вида «#RRGGBB»: альфа не показывается, потому что -/// за прозрачность отвечает отдельная настройка. +/// A colour into a "#RRGGBB" notation: the alpha is not shown, because +/// transparency is a separate setting. /// public sealed class ColorToHexConverter : IValueConverter { @@ -40,7 +40,7 @@ public sealed class ColorToHexConverter : IValueConverter } /// -/// Цвет в кисть — для образцов палитры в списке. +/// A colour into a brush — for the palette swatches in the list. /// public sealed class ColorToBrushConverter : IValueConverter { diff --git a/CursorLang/Views/LayoutPopupWindow.xaml.cs b/CursorLang/Views/LayoutPopupWindow.xaml.cs index 7e68645..fc1e4d2 100644 --- a/CursorLang/Views/LayoutPopupWindow.xaml.cs +++ b/CursorLang/Views/LayoutPopupWindow.xaml.cs @@ -7,9 +7,9 @@ using CursorLang.ViewModels; namespace CursorLang.Views; /// -/// Всплывающая подсказка с коротким именем раскладки. -/// Отвечает только за показ, размер и место на экране: когда её убрать, -/// решает . +/// The popup with the short name of the layout. +/// Responsible only for showing it, its size and its place on screen: when to take it +/// down is decided by . /// public partial class LayoutPopupWindow : Window { @@ -23,25 +23,25 @@ public partial class LayoutPopupWindow : Window DataContext = viewModel; _settings = settings; - // Создаём окно заранее, чтобы задать его границы до первого показа: - // иначе оно на мгновение появляется в размере по умолчанию + // We create the window in advance to set its bounds before the first show: + // otherwise it appears at the default size for a moment new WindowInteropHelper(this).EnsureHandle(); } /// - /// Показывает подсказку в месте, заданном настройками. + /// Shows the popup at the place set by the settings. /// public void ShowPopup() { MeasureContent(); - // Уже созданное окно ставим на место до показа, чтобы оно не мелькнуло - // на старой позиции; при самом первом вызове хэндла ещё нет + // An already created window is put in place before the show so that it does + // not flash in its old position; on the very first call there is no handle yet ApplyBounds(); Show(); - // Повторяем после показа: до него окно подчиняется системному минимальному - // размеру окна и получается заметно крупнее текста + // We repeat it after the show: before that the window is subject to the system + // minimum window size and comes out noticeably larger than its text ApplyBounds(); } @@ -52,9 +52,9 @@ public partial class LayoutPopupWindow : Window PopupWindowNative.MakePassive(new WindowInteropHelper(this).Handle); } - // Размер содержимого нужен до показа: от него зависит и размер окна, - // и позиция для углов. Рамок у окна нет, поэтому размер окна равен размеру - // содержимого + // The content size is needed before the show: both the window size and the corner + // position depend on it. The window has no frame, so the window size equals the + // content size private void MeasureContent() { var content = (FrameworkElement)Content; @@ -63,9 +63,9 @@ public partial class LayoutPopupWindow : Window _contentSize = content.DesiredSize; } - // Всё считается в физических пикселях: у мониторов разный масштаб, а - // Window.Left/Top/Width/Height пересчитываются по DPI того монитора, - // где окно сейчас, и на соседнем мониторе дают промах + // Everything is computed in physical pixels: monitors have different scaling, + // while Window.Left/Top/Width/Height are converted using the DPI of the monitor + // the window is on right now, which misses the target on a neighbouring monitor private void ApplyBounds() { IntPtr handle = new WindowInteropHelper(this).Handle; @@ -84,8 +84,8 @@ public partial class LayoutPopupWindow : Window } } - // Точка привязки: каретка в поле ввода либо курсор мыши. Курсор — это - // прямоугольник нулевого размера, поэтому расчёт углов у них общий + // The anchor point: the caret in the input field or the mouse cursor. The cursor + // is a rectangle of zero size, so the corner computation is shared by both private PopupWindowNative.Rect GetAnchor() { if (_settings.PlacementMode == PopupPlacementMode.AtCaret && @@ -109,7 +109,7 @@ public partial class LayoutPopupWindow : Window var anchorPoint = new PopupWindowNative.Point { X = anchor.Left, Y = anchor.Top }; double scale = PopupWindowNative.GetScaleAt(anchorPoint); - // У каждого режима привязки своя сторона и свой отступ + // Every anchor mode has a side and an offset of its own bool atCaret = _settings.PlacementMode == PopupPlacementMode.AtCaret; AnchorSide side = atCaret ? _settings.CaretSide : _settings.CursorSide; int offset = ToPixels(atCaret ? _settings.CaretOffset : _settings.CursorOffset, scale); diff --git a/CursorLang/Views/MainWindow.xaml b/CursorLang/Views/MainWindow.xaml index 3b41b14..19d86bf 100644 --- a/CursorLang/Views/MainWindow.xaml +++ b/CursorLang/Views/MainWindow.xaml @@ -35,6 +35,9 @@ + diff --git a/CursorLang/Views/MainWindow.xaml.cs b/CursorLang/Views/MainWindow.xaml.cs index 9663ddb..aa09bd5 100644 --- a/CursorLang/Views/MainWindow.xaml.cs +++ b/CursorLang/Views/MainWindow.xaml.cs @@ -5,7 +5,7 @@ using CursorLang.ViewModels; namespace CursorLang.Views; /// -/// Окно настроек приложения. +/// The settings window of the application. /// public partial class MainWindow : Window {