fixed comment into code
This commit is contained in:
@@ -6,12 +6,12 @@ using Accessibility;
|
|||||||
namespace CursorLang.Interop;
|
namespace CursorLang.Interop;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Определяет положение каретки в активном поле ввода — в том числе в чужом приложении.
|
/// Locates the caret in the active input field — including one in another application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Единого способа нет: классические Win32-приложения заводят системную каретку,
|
/// There is no single way to do it: classic Win32 applications create a system caret,
|
||||||
/// а Chrome, Electron и прочие рисуют её сами и сообщают положение только через
|
/// 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>
|
/// </remarks>
|
||||||
internal static class CaretNative
|
internal static class CaretNative
|
||||||
{
|
{
|
||||||
@@ -32,8 +32,8 @@ internal static class CaretNative
|
|||||||
private const int CHILDID_SELF = 0;
|
private const int CHILDID_SELF = 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Прямоугольник каретки в пикселях экрана или <c>null</c>, если активное
|
/// The caret rectangle in screen pixels, or <c>null</c> when the active
|
||||||
/// приложение не сообщает её положение.
|
/// application does not report its position.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static PopupWindowNative.Rect? TryGetCaretRect()
|
internal static PopupWindowNative.Rect? TryGetCaretRect()
|
||||||
{
|
{
|
||||||
@@ -50,13 +50,13 @@ internal static class CaretNative
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Отсеивает заведомо неверные координаты.
|
/// Filters out obviously wrong coordinates.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <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>
|
/// </remarks>
|
||||||
private static PopupWindowNative.Rect? Validate(PopupWindowNative.Rect caret, IntPtr hwndFocus)
|
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.Left >= outer.Left && inner.Right <= outer.Right &&
|
||||||
inner.Top >= outer.Top && inner.Bottom <= outer.Bottom;
|
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);
|
private static readonly TimeSpan AutomationTimeout = TimeSpan.FromMilliseconds(150);
|
||||||
|
|
||||||
// Браузеры и другие приложения на своих движках рисуют каретку сами и
|
// Browsers and other applications with their own rendering engines draw the caret
|
||||||
// сообщают её положение только через UI Automation. Запрос идёт в чужой
|
// 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()
|
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);
|
Task<PopupWindowNative.Rect?> query = Task.Run(QueryAutomationCaret);
|
||||||
return query.Wait(AutomationTimeout) ? query.Result : null;
|
return query.Wait(AutomationTimeout) ? query.Result : null;
|
||||||
}
|
}
|
||||||
@@ -117,8 +117,8 @@ internal static class CaretNative
|
|||||||
return null;
|
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();
|
TextPatternRange range = selection[0].Clone();
|
||||||
range.ExpandToEnclosingUnit(TextUnit.Character);
|
range.ExpandToEnclosingUnit(TextUnit.Character);
|
||||||
|
|
||||||
@@ -141,12 +141,12 @@ internal static class CaretNative
|
|||||||
or InvalidOperationException
|
or InvalidOperationException
|
||||||
or COMException)
|
or COMException)
|
||||||
{
|
{
|
||||||
// Приложение закрылось или не отвечает — подсказку это ронять не должно
|
// The application closed or stopped responding — that must not take the popup down
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Системная каретка: координаты приходят относительно окна, которому она принадлежит
|
// The system caret: its coordinates come relative to the window that owns it
|
||||||
private static PopupWindowNative.Rect? TryGetSystemCaret(ForegroundInputNative.GuiThreadInfo info)
|
private static PopupWindowNative.Rect? TryGetSystemCaret(ForegroundInputNative.GuiThreadInfo info)
|
||||||
{
|
{
|
||||||
if (info.hwndCaret == IntPtr.Zero || IsEmpty(info.rcCaret))
|
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)
|
private static PopupWindowNative.Rect? TryGetAccessibleCaret(IntPtr hwndFocus)
|
||||||
{
|
{
|
||||||
if (hwndFocus == IntPtr.Zero)
|
if (hwndFocus == IntPtr.Zero)
|
||||||
@@ -199,7 +199,7 @@ internal static class CaretNative
|
|||||||
}
|
}
|
||||||
catch (COMException)
|
catch (COMException)
|
||||||
{
|
{
|
||||||
// Приложение объявило поддержку, но положение не отдало
|
// The application declared support but did not report the position
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -208,7 +208,7 @@ internal static class CaretNative
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Когда каретки нет, её прямоугольник приходит нулевой высоты.
|
// 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
|
||||||
private static bool IsEmpty(PopupWindowNative.Rect rect) => rect.Bottom - rect.Top <= 0;
|
internal static bool IsEmpty(PopupWindowNative.Rect rect) => rect.Bottom - rect.Top <= 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ using System.Runtime.InteropServices;
|
|||||||
namespace CursorLang.Interop;
|
namespace CursorLang.Interop;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Сведения о вводе в активном приложении: какое окно держит фокус клавиатуры
|
/// Input details of the active application: which window holds keyboard focus
|
||||||
/// и где находится каретка.
|
/// and where the caret is.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class ForegroundInputNative
|
internal static class ForegroundInputNative
|
||||||
{
|
{
|
||||||
@@ -26,12 +26,12 @@ internal static class ForegroundInputNative
|
|||||||
private static extern bool GetGUIThreadInfo(uint idThread, ref GuiThreadInfo lpgui);
|
private static extern bool GetGUIThreadInfo(uint idThread, ref GuiThreadInfo lpgui);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Состояние ввода потока переднего плана.
|
/// The input state of the foreground thread.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <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>
|
/// </remarks>
|
||||||
internal static bool TryGetInfo(out GuiThreadInfo info)
|
internal static bool TryGetInfo(out GuiThreadInfo info)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,21 +3,21 @@ using System.Runtime.InteropServices;
|
|||||||
namespace CursorLang.Interop;
|
namespace CursorLang.Interop;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Право выводить окно на передний план.
|
/// The right to bring a window to the foreground.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class ForegroundPermissionNative
|
internal static class ForegroundPermissionNative
|
||||||
{
|
{
|
||||||
/// <summary>ASFW_ANY — право получает любой процесс.</summary>
|
/// <summary>ASFW_ANY — any process gets the right.</summary>
|
||||||
private const uint AnyProcess = 0xFFFFFFFF;
|
private const uint AnyProcess = 0xFFFFFFFF;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Уступает своё право вывести окно на передний план другим процессам.
|
/// Gives up our right to bring a window to the foreground in favour of other processes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <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>
|
/// </remarks>
|
||||||
internal static void GrantToAnyProcess() => AllowSetForegroundWindow(AnyProcess);
|
internal static void GrantToAnyProcess() => AllowSetForegroundWindow(AnyProcess);
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ using System.Runtime.InteropServices;
|
|||||||
namespace CursorLang.Interop;
|
namespace CursorLang.Interop;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Win32 API для определения раскладки активного приложения.
|
/// Win32 API for reading the layout of the active application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class KeyboardLayoutNative
|
internal static class KeyboardLayoutNative
|
||||||
{
|
{
|
||||||
private const uint WmInputLangChangeRequest = 0x0050;
|
private const uint WmInputLangChangeRequest = 0x0050;
|
||||||
|
|
||||||
/// <summary>Взять следующую раскладку из системного списка.</summary>
|
/// <summary>Take the next layout from the system list.</summary>
|
||||||
private static readonly IntPtr InputLangChangeForward = new(0x0002);
|
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);
|
private static readonly IntPtr HklNext = new(1);
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
@@ -28,17 +28,17 @@ internal static class KeyboardLayoutNative
|
|||||||
private static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
private static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Раскладка, которой сейчас печатает пользователь.
|
/// The layout the user is currently typing with.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static int GetActiveLocaleId() => GetLocaleIdOf(GetInputWindow());
|
internal static int GetActiveLocaleId() => GetLocaleIdOf(GetInputWindow());
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Просит активное приложение перейти на следующую раскладку из системного списка.
|
/// Asks the active application to switch to the next layout from the system list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Синтезировать системное сочетание вроде Alt+Shift не годится: его назначение
|
/// Synthesizing a system shortcut such as Alt+Shift will not do: the user can
|
||||||
/// пользователь меняет в настройках Windows и может отключить вовсе. Просьба
|
/// 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>
|
/// </remarks>
|
||||||
internal static void RequestNextLayout()
|
internal static void RequestNextLayout()
|
||||||
{
|
{
|
||||||
@@ -48,19 +48,19 @@ internal static class KeyboardLayoutNative
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Оба параметра означают одно и то же: разные версии Windows и разные
|
// Both parameters mean the same thing: different Windows versions and different
|
||||||
// библиотеки интерфейса смотрят то на флаг, то на lParam
|
// UI frameworks look either at the flag or at lParam
|
||||||
PostMessage(target, WmInputLangChangeRequest, InputLangChangeForward, HklNext);
|
PostMessage(target, WmInputLangChangeRequest, InputLangChangeForward, HklNext);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Окно, которому принадлежит ввод с клавиатуры.
|
/// The window that owns keyboard input.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Спрашиваем окно с фокусом клавиатуры, а не окно верхнего плана: у Блокнота
|
/// We ask the window with keyboard focus rather than the foreground window: in
|
||||||
/// Windows 11, меню «Пуск» и прочих приложений на WinUI поле ввода живёт в
|
/// 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>
|
/// </remarks>
|
||||||
private static IntPtr GetInputWindow()
|
private static IntPtr GetInputWindow()
|
||||||
{
|
{
|
||||||
@@ -74,8 +74,8 @@ internal static class KeyboardLayoutNative
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
internal static int GetLocaleIdOf(IntPtr hWnd)
|
internal static int GetLocaleIdOf(IntPtr hWnd)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,22 +3,22 @@ using System.Runtime.InteropServices;
|
|||||||
namespace CursorLang.Interop;
|
namespace CursorLang.Interop;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Ставится только низкоуровневый хук: обычный WH_KEYBOARD требует внедрения
|
/// Only the low-level hook is installed: a regular WH_KEYBOARD requires injecting a
|
||||||
/// DLL в чужие процессы, что для managed-кода невозможно. Обратный вызов
|
/// 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
|
||||||
/// тайм-аута Windows молча снимает хук.
|
/// silently removes the hook.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal sealed class LowLevelKeyboardHook : IDisposable
|
internal sealed class LowLevelKeyboardHook : IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
internal delegate bool KeyFilter(int virtualKey, bool isKeyDown);
|
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 WmSysKeyDown = 0x0104;
|
||||||
private const int WmSysKeyUp = 0x0105;
|
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 const uint LowLevelKeyHookFlagInjected = 0x10;
|
||||||
|
|
||||||
private readonly KeyFilter _filter;
|
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 readonly HookProc _callback;
|
||||||
|
|
||||||
private IntPtr _handle;
|
private IntPtr _handle;
|
||||||
@@ -53,7 +53,7 @@ internal sealed class LowLevelKeyboardHook : IDisposable
|
|||||||
internal bool IsInstalled => _handle != IntPtr.Zero;
|
internal bool IsInstalled => _handle != IntPtr.Zero;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ставит хук. Возвращает <c>false</c>, если система отказала.
|
/// Installs the hook. Returns <c>false</c> when the system refuses.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal bool Install()
|
internal bool Install()
|
||||||
{
|
{
|
||||||
@@ -104,14 +104,14 @@ internal sealed class LowLevelKeyboardHook : IDisposable
|
|||||||
|
|
||||||
var data = Marshal.PtrToStructure<KeyboardHookData>(lParam);
|
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;
|
bool injected = (data.flags & LowLevelKeyHookFlagInjected) != 0;
|
||||||
|
|
||||||
if ((isKeyDown || isKeyUp) && !injected && _filter((int)data.vkCode, isKeyDown))
|
if ((isKeyDown || isKeyUp) && !injected && _filter((int)data.vkCode, isKeyDown))
|
||||||
{
|
{
|
||||||
// Ненулевой результат вместо CallNextHookEx обрывает цепочку: событие
|
// A non-zero result instead of CallNextHookEx breaks the chain: the event
|
||||||
// не дойдёт ни до приложения, ни до обработчика регистра в Windows
|
// will reach neither the application nor the Windows caps-lock handler
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ using System.Runtime.InteropServices;
|
|||||||
namespace CursorLang.Interop;
|
namespace CursorLang.Interop;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
internal static class PopupWindowNative
|
internal static class PopupWindowNative
|
||||||
{
|
{
|
||||||
@@ -62,9 +62,9 @@ internal static class PopupWindowNative
|
|||||||
}
|
}
|
||||||
|
|
||||||
private const int GWL_EXSTYLE = -20;
|
private const int GWL_EXSTYLE = -20;
|
||||||
// Окно не забирает фокус у активного приложения
|
// The window does not take focus away from the active application
|
||||||
private const int WS_EX_NOACTIVATE = 0x08000000;
|
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 int WS_EX_TOOLWINDOW = 0x00000080;
|
||||||
|
|
||||||
private const uint SWP_NOSIZE = 0x0001;
|
private const uint SWP_NOSIZE = 0x0001;
|
||||||
@@ -81,8 +81,8 @@ internal static class PopupWindowNative
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
internal static void MakePassive(IntPtr hWnd)
|
internal static void MakePassive(IntPtr hWnd)
|
||||||
{
|
{
|
||||||
@@ -91,10 +91,10 @@ internal static class PopupWindowNative
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Двигает окно в точку экрана, не меняя размер и порядок окон.
|
/// 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 пересчитываются по DPI того монитора, где окно сейчас,
|
/// 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>
|
/// </summary>
|
||||||
internal static void MoveTo(IntPtr hWnd, int x, int y)
|
internal static void MoveTo(IntPtr hWnd, int x, int y)
|
||||||
{
|
{
|
||||||
@@ -102,26 +102,27 @@ internal static class PopupWindowNative
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Задаёт положение и размер окна в физических пикселях.
|
/// Sets the window position and size in physical pixels.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Размер выставляется именно так, а не через Width/Height: при первом показе
|
/// The size is set this way rather than through Width/Height: on the first show the
|
||||||
/// окно ещё подчиняется системному минимальному размеру окна (SM_CXMIN×SM_CYMIN)
|
/// 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
|
||||||
/// показано и стало popup-окном, на которое это ограничение не действует.
|
/// called the window is already shown and has become a popup window, which that
|
||||||
|
/// restriction does not apply to.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal static void SetBounds(IntPtr hWnd, int x, int y, int width, int height)
|
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);
|
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) =>
|
internal static double GetScaleAt(Point point) =>
|
||||||
GetScaleOf(MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST));
|
GetScaleOf(MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST));
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
internal static (Rect WorkArea, double Scale) GetActiveMonitorWorkArea()
|
internal static (Rect WorkArea, double Scale) GetActiveMonitorWorkArea()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ using System.Runtime.InteropServices;
|
|||||||
namespace CursorLang.Interop;
|
namespace CursorLang.Interop;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Границы берутся у системы, а не из <c>Window.Left/Top/Width/Height</c>:
|
/// The bounds are taken from the system rather than from <c>Window.Left/Top/Width/Height</c>:
|
||||||
/// высота окна подстраивается под содержимое, и WPF пересчитывает эти свойства
|
/// the window height adapts to its content, and WPF converts those properties using the
|
||||||
/// по DPI монитора, а рабочая область монитора приходит в пикселях. Считать
|
/// 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>
|
/// </remarks>
|
||||||
internal static class WindowPlacementNative
|
internal static class WindowPlacementNative
|
||||||
{
|
{
|
||||||
@@ -34,13 +34,13 @@ internal static class WindowPlacementNative
|
|||||||
|
|
||||||
private const uint MONITOR_DEFAULTTONEAREST = 2;
|
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) =>
|
internal static PopupWindowNative.Rect? TryGetBounds(IntPtr hWnd) =>
|
||||||
GetWindowRect(hWnd, out PopupWindowNative.Rect bounds) ? bounds : null;
|
GetWindowRect(hWnd, out PopupWindowNative.Rect bounds) ? bounds : null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Рабочая область — без панели задач — того монитора, на котором
|
/// The work area — without the taskbar — of the monitor that holds the
|
||||||
/// прямоугольник находится целиком или хотя бы большей частью.
|
/// rectangle entirely, or at least most of it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static PopupWindowNative.Rect? TryGetWorkAreaNear(PopupWindowNative.Rect rect)
|
internal static PopupWindowNative.Rect? TryGetWorkAreaNear(PopupWindowNative.Rect rect)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ using System.Runtime.InteropServices;
|
|||||||
namespace CursorLang.Interop;
|
namespace CursorLang.Interop;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
internal static class WindowThemeNative
|
internal static class WindowThemeNative
|
||||||
{
|
{
|
||||||
@@ -14,8 +14,8 @@ internal static class WindowThemeNative
|
|||||||
private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
|
private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
internal static void SetDarkTitleBar(IntPtr hWnd, bool isDark)
|
internal static void SetDarkTitleBar(IntPtr hWnd, bool isDark)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,67 +5,68 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
namespace CursorLang.Models;
|
namespace CursorLang.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Настройки приложения. Все изменения применяются на лету: подсказка и окно
|
/// The application settings. Every change applies on the fly: the popup and the
|
||||||
/// настроек привязаны к этим свойствам, а <c>SettingsService</c> сохраняет их на диск.
|
/// settings window are bound to these properties, and <c>SettingsService</c> saves
|
||||||
|
/// them to disk.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class AppSettings : ObservableObject
|
public sealed partial class AppSettings : ObservableObject
|
||||||
{
|
{
|
||||||
/// <summary>Язык интерфейса в виде кода культуры: «ru», «en».</summary>
|
/// <summary>The interface language as a culture code: "ru", "en".</summary>
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _language = "en";
|
private string _language = "en";
|
||||||
|
|
||||||
/// <summary>Оформление окна настроек.</summary>
|
/// <summary>The look of the settings window.</summary>
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private AppTheme _theme = AppTheme.System;
|
private AppTheme _theme = AppTheme.System;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private PopupPlacementMode _placementMode = PopupPlacementMode.AtCursor;
|
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]
|
[ObservableProperty]
|
||||||
private AnchorSide _cursorSide = AnchorSide.BottomRight;
|
private AnchorSide _cursorSide = AnchorSide.BottomRight;
|
||||||
|
|
||||||
/// <summary>Отступ от курсора в единицах WPF.</summary>
|
/// <summary>The offset from the cursor in WPF units.</summary>
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private double _cursorOffset = 16;
|
private double _cursorOffset = 16;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private AnchorSide _caretSide = AnchorSide.BottomRight;
|
private AnchorSide _caretSide = AnchorSide.BottomRight;
|
||||||
|
|
||||||
/// <summary>Отступ от каретки в единицах WPF.</summary>
|
/// <summary>The offset from the caret in WPF units.</summary>
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private double _caretOffset = 16;
|
private double _caretOffset = 16;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private ScreenPosition _screenPosition = ScreenPosition.BottomRight;
|
private ScreenPosition _screenPosition = ScreenPosition.BottomRight;
|
||||||
|
|
||||||
/// <summary>Отступ от края монитора в единицах WPF.</summary>
|
/// <summary>The offset from the monitor edge in WPF units.</summary>
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private double _screenMargin = 24;
|
private double _screenMargin = 24;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private double _fontSize = 20;
|
private double _fontSize = 20;
|
||||||
|
|
||||||
/// <summary>Непрозрачность подсказки: 1.0 — полностью непрозрачная.</summary>
|
/// <summary>The popup opacity: 1.0 is fully opaque.</summary>
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private double _opacity = 0.9;
|
private double _opacity = 0.9;
|
||||||
|
|
||||||
/// <summary>Сколько подсказка держится на экране, в миллисекундах.</summary>
|
/// <summary>How long the popup stays on screen, in milliseconds.</summary>
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private double _durationMilliseconds = 500;
|
private double _durationMilliseconds = 500;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Перехватывать 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _useCapsLockHotkey;
|
private bool _useCapsLockHotkey;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// С какого времени удержания Caps Lock переключение отменяется, в миллисекундах.
|
/// After how long a Caps Lock hold cancels the switch, in milliseconds.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private double _capsLockHoldMilliseconds = 300;
|
private double _capsLockHoldMilliseconds = 300;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
namespace CursorLang.Models;
|
namespace CursorLang.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Оформление окна настроек. По умолчанию приложение следует теме 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum AppTheme
|
public enum AppTheme
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ using System.Globalization;
|
|||||||
namespace CursorLang.Models;
|
namespace CursorLang.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Раскладка клавиатуры в удобном для отображения виде.
|
/// A keyboard layout in a form convenient for display.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="LocaleId">Идентификатор локали (младшее слово HKL).</param>
|
/// <param name="LocaleId">The locale identifier (the low word of HKL).</param>
|
||||||
/// <param name="ShortName">Короткое имя для подсказки у курсора, например «RU».</param>
|
/// <param name="ShortName">A short name for the popup at the cursor, "RU" for instance.</param>
|
||||||
/// <param name="DisplayName">Полное имя, например «RU — русский (Россия)».</param>
|
/// <param name="DisplayName">The full name, "RU — русский (Россия)" for instance.</param>
|
||||||
public sealed record KeyboardLayout(int LocaleId, string ShortName, string DisplayName)
|
public sealed record KeyboardLayout(int LocaleId, string ShortName, string DisplayName)
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Строит модель по идентификатору локали. Неизвестные локали не являются
|
/// Builds the model from a locale identifier. Unknown locales are not an
|
||||||
/// ошибкой: для них показываем сам идентификатор.
|
/// error: for them we show the identifier itself.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static KeyboardLayout FromLocaleId(int localeId)
|
public static KeyboardLayout FromLocaleId(int localeId)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
namespace CursorLang.Models;
|
namespace CursorLang.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Почему изменилась текущая раскладка.
|
/// Why the current layout has changed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum LayoutChangeReason
|
public enum LayoutChangeReason
|
||||||
{
|
{
|
||||||
/// <summary>Пользователь переключил раскладку в активном приложении.</summary>
|
/// <summary>The user switched the layout in the active application.</summary>
|
||||||
UserSwitched,
|
UserSwitched,
|
||||||
|
|
||||||
/// <summary>Пользователь перешёл в другое приложение, где своя раскладка.</summary>
|
/// <summary>The user moved to another application that has a layout of its own.</summary>
|
||||||
ApplicationSwitched,
|
ApplicationSwitched,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Данные события смены раскладки.
|
/// The data of a layout change event.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class LayoutChangedEventArgs(KeyboardLayout layout, LayoutChangeReason reason) : EventArgs
|
public sealed class LayoutChangedEventArgs(KeyboardLayout layout, LayoutChangeReason reason) : EventArgs
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
namespace CursorLang.Models;
|
namespace CursorLang.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Способ выбора места для подсказки.
|
/// How the place for the popup is chosen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum PopupPlacementMode
|
public enum PopupPlacementMode
|
||||||
{
|
{
|
||||||
/// <summary>Рядом с курсором мыши.</summary>
|
/// <summary>Next to the mouse cursor.</summary>
|
||||||
AtCursor,
|
AtCursor,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Рядом с кареткой в активном поле ввода. Если приложение не сообщает
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
AtCaret,
|
AtCaret,
|
||||||
|
|
||||||
/// <summary>В заданной точке монитора с активным окном.</summary>
|
/// <summary>At a fixed point of the monitor holding the active window.</summary>
|
||||||
FixedPoint,
|
FixedPoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// С какой стороны от курсора или каретки показывать подсказку.
|
/// Which side of the cursor or the caret to show the popup on.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum AnchorSide
|
public enum AnchorSide
|
||||||
{
|
{
|
||||||
@@ -32,7 +32,7 @@ public enum AnchorSide
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Место на мониторе для режима <see cref="PopupPlacementMode.FixedPoint"/>.
|
/// The place on the monitor for the <see cref="PopupPlacementMode.FixedPoint"/> mode.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum ScreenPosition
|
public enum ScreenPosition
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ using CursorLang.Models;
|
|||||||
namespace CursorLang.Services;
|
namespace CursorLang.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Держит системный перехват Caps Lock и отличает короткое нажатие от удержания.
|
/// Holds the system Caps Lock hook and tells a short tap from a hold.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Различить их можно только по факту отпускания клавиши, поэтому перехватываются
|
/// 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
|
||||||
/// смену регистра: Windows переключает Caps Lock по событию нажатия, и пропустить
|
/// case change: Windows toggles Caps Lock on the press event, and letting it through
|
||||||
/// его «на всякий случай» нельзя.
|
/// "just in case" is not an option.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable
|
public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable
|
||||||
{
|
{
|
||||||
@@ -28,7 +28,7 @@ public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable
|
|||||||
public CapsLockHotkeyService(AppSettings settings)
|
public CapsLockHotkeyService(AppSettings settings)
|
||||||
{
|
{
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
_hook = new LowLevelKeyboardHook(OnKeyEvent);
|
_hook = new LowLevelKeyboardHook(HandleKeyEvent);
|
||||||
_holdTimer.Tick += OnHoldTimerTick;
|
_holdTimer.Tick += OnHoldTimerTick;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,9 +48,9 @@ public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable
|
|||||||
ResetPress();
|
ResetPress();
|
||||||
}
|
}
|
||||||
|
|
||||||
// При закрытии приложения событий уже никто не ждёт, поэтому в отличие
|
// When the application is closing, nobody is waiting for events any more, so
|
||||||
// от Stop состояние сбрасывается молча: очередь диспетчера в этот момент
|
// unlike in Stop the state is reset quietly: the dispatcher queue may be shut
|
||||||
// может быть закрыта
|
// down by that moment
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_holdTimer.Tick -= OnHoldTimerTick;
|
_holdTimer.Tick -= OnHoldTimerTick;
|
||||||
@@ -60,10 +60,12 @@ public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable
|
|||||||
_hook.Dispose();
|
_hook.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вызывается системным хуком, то есть внутри разбора очереди сообщений.
|
// 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.
|
||||||
private bool OnKeyEvent(int virtualKey, bool isKeyDown)
|
// 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)
|
if (virtualKey != VirtualKeyCapsLock)
|
||||||
{
|
{
|
||||||
@@ -72,13 +74,13 @@ public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable
|
|||||||
|
|
||||||
if (isKeyDown)
|
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)
|
if (!_isPressed)
|
||||||
{
|
{
|
||||||
_isPressed = true;
|
_isPressed = true;
|
||||||
|
|
||||||
// Порог читаем при каждом нажатии: его меняют в настройках на лету
|
// The threshold is read on every press: it is changed in the settings on the fly
|
||||||
_holdTimer.Interval = _settings.CapsLockHoldDelay;
|
_holdTimer.Interval = _settings.CapsLockHoldDelay;
|
||||||
_holdTimer.Start();
|
_holdTimer.Start();
|
||||||
}
|
}
|
||||||
@@ -109,8 +111,8 @@ public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable
|
|||||||
HoldStarted?.Invoke(this, EventArgs.Empty);
|
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)
|
private void Notify(EventHandler? handler)
|
||||||
{
|
{
|
||||||
if (handler is not null)
|
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()
|
private void ResetPress()
|
||||||
{
|
{
|
||||||
_isPressed = false;
|
_isPressed = false;
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ using CursorLang.Models;
|
|||||||
namespace CursorLang.Services;
|
namespace CursorLang.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Превращает нажатия 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class CapsLockSwitchCoordinator : IDisposable
|
public sealed class CapsLockSwitchCoordinator : IDisposable
|
||||||
{
|
{
|
||||||
@@ -68,8 +68,8 @@ public sealed class CapsLockSwitchCoordinator : IDisposable
|
|||||||
|
|
||||||
private void OnTapped(object? sender, EventArgs e) => _layoutService.SwitchToNext();
|
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) =>
|
private void OnHoldStarted(object? sender, EventArgs e) =>
|
||||||
_popupService.ShowUntilHidden(_layoutService.Current);
|
_popupService.ShowUntilHidden(_layoutService.Current);
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
namespace CursorLang.Services;
|
namespace CursorLang.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Перехватывает 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ICapsLockHotkeyService
|
public interface ICapsLockHotkeyService
|
||||||
{
|
{
|
||||||
/// <summary>Короткое нажатие: клавишу отпустили до порога удержания.</summary>
|
/// <summary>A short press: the key was released before the hold threshold.</summary>
|
||||||
event EventHandler? Tapped;
|
event EventHandler? Tapped;
|
||||||
|
|
||||||
/// <summary>Порог удержания пройден, клавишу всё ещё держат.</summary>
|
/// <summary>The hold threshold has passed, the key is still held.</summary>
|
||||||
event EventHandler? HoldStarted;
|
event EventHandler? HoldStarted;
|
||||||
|
|
||||||
/// <summary>Удержание закончилось: клавишу отпустили.</summary>
|
/// <summary>The hold is over: the key was released.</summary>
|
||||||
event EventHandler? HoldEnded;
|
event EventHandler? HoldEnded;
|
||||||
|
|
||||||
/// <summary>Стоит ли перехват прямо сейчас.</summary>
|
/// <summary>Whether the hook is installed right now.</summary>
|
||||||
bool IsRunning { get; }
|
bool IsRunning { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Начинает перехват. Вызывать нужно из потока пользовательского интерфейса:
|
/// Starts intercepting. Must be called from the user interface thread:
|
||||||
/// системный хук клавиатуры работает только на потоке с циклом сообщений.
|
/// a system keyboard hook works only on a thread with a message loop.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void Start();
|
void Start();
|
||||||
|
|
||||||
/// <summary>Снимает перехват, возвращая клавише её обычное поведение.</summary>
|
/// <summary>Removes the hook, giving the key its usual behaviour back.</summary>
|
||||||
void Stop();
|
void Stop();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ using CursorLang.Models;
|
|||||||
namespace CursorLang.Services;
|
namespace CursorLang.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Следит за раскладкой активного окна — в том числе в чужих приложениях —
|
/// Tracks the layout of the active window — including in other applications —
|
||||||
/// и умеет её переключать.
|
/// and can switch it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IKeyboardLayoutService
|
public interface IKeyboardLayoutService
|
||||||
{
|
{
|
||||||
/// <summary>Раскладка активного окна на текущий момент.</summary>
|
/// <summary>The layout of the active window at the moment.</summary>
|
||||||
KeyboardLayout Current { get; }
|
KeyboardLayout Current { get; }
|
||||||
|
|
||||||
event EventHandler<LayoutChangedEventArgs>? LayoutChanged;
|
event EventHandler<LayoutChangedEventArgs>? LayoutChanged;
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ using CursorLang.Models;
|
|||||||
namespace CursorLang.Services;
|
namespace CursorLang.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Показывает подсказку с раскладкой у курсора.
|
/// Shows the layout popup at the cursor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ILayoutPopupService
|
public interface ILayoutPopupService
|
||||||
{
|
{
|
||||||
/// <summary>Показывает подсказку и убирает её через заданное в настройках время.</summary>
|
/// <summary>Shows the popup and takes it down after the time set in the settings.</summary>
|
||||||
void Show(KeyboardLayout layout);
|
void Show(KeyboardLayout layout);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Показывает подсказку до явного вызова <see cref="Hide"/>: нужна там, где
|
/// Shows the popup until <see cref="Hide"/> is called explicitly: needed where the
|
||||||
/// время показа задаёт не таймер, а действие пользователя.
|
/// show time is set by a user action rather than by a timer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void ShowUntilHidden(KeyboardLayout layout);
|
void ShowUntilHidden(KeyboardLayout layout);
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,21 @@ using System.ComponentModel;
|
|||||||
|
|
||||||
namespace CursorLang.Services;
|
namespace CursorLang.Services;
|
||||||
|
|
||||||
/// <summary>Язык интерфейса для выбора в настройках.</summary>
|
/// <summary>An interface language to choose from in the settings.</summary>
|
||||||
/// <param name="Code">Код культуры: «ru», «en».</param>
|
/// <param name="Code">The culture code: "ru", "en".</param>
|
||||||
/// <param name="DisplayName">Название на самом этом языке.</param>
|
/// <param name="DisplayName">The name in that very language.</param>
|
||||||
public sealed record LanguageOption(string Code, string DisplayName)
|
public sealed record LanguageOption(string Code, string DisplayName)
|
||||||
{
|
{
|
||||||
// Средства доступности берут имя элемента списка отсюда
|
// Accessibility tools take the name of the list item from here
|
||||||
public override string ToString() => DisplayName;
|
public override string ToString() => DisplayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Даёт строки интерфейса и умеет менять язык без перезапуска.
|
/// Provides the interface strings and can change the language without a restart.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ILocalizationService : INotifyPropertyChanged
|
public interface ILocalizationService : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
/// <summary>Строка по ключу ресурса. Привязки обновляются при смене языка.</summary>
|
/// <summary>The string for a resource key. Bindings update when the language changes.</summary>
|
||||||
string this[string key] { get; }
|
string this[string key] { get; }
|
||||||
|
|
||||||
IReadOnlyList<LanguageOption> AvailableLanguages { get; }
|
IReadOnlyList<LanguageOption> AvailableLanguages { get; }
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ using CursorLang.Models;
|
|||||||
namespace CursorLang.Services;
|
namespace CursorLang.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Применяет светлое или тёмное оформление к окнам приложения.
|
/// Applies the light or the dark look to the windows of the application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IThemeService
|
public interface IThemeService
|
||||||
{
|
{
|
||||||
/// <summary>Тема, действующая сейчас.</summary>
|
/// <summary>The theme in effect right now.</summary>
|
||||||
AppTheme CurrentTheme { get; }
|
AppTheme CurrentTheme { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Подключает окно к смене темы: заголовок окна рисует 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void Register(Window window);
|
void Register(Window window);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,23 +5,24 @@ using CursorLang.Models;
|
|||||||
namespace CursorLang.Services;
|
namespace CursorLang.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Настройки слежения за раскладкой.
|
/// The settings of layout tracking.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class KeyboardLayoutOptions
|
public sealed class KeyboardLayoutOptions
|
||||||
{
|
{
|
||||||
/// <summary>Как часто проверять раскладку активного окна.</summary>
|
/// <summary>How often to check the layout of the active window.</summary>
|
||||||
public TimeSpan PollInterval { get; init; } = TimeSpan.FromMilliseconds(150);
|
public TimeSpan PollInterval { get; init; } = TimeSpan.FromMilliseconds(150);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Опрашивает активное окно по таймеру.
|
/// Polls the active window on a timer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Опрос выбран не от простоты: событийных способов узнать о смене раскладки
|
/// Polling was chosen not for simplicity: there is no event-based way to learn about
|
||||||
/// в чужом процессе из managed-кода нет. HSHELL_LANGUAGE от RegisterShellHookWindow
|
/// a layout change in another process from managed code. HSHELL_LANGUAGE from
|
||||||
/// в Windows 10/11 не приходит, а уведомления TSF (ITfLanguageProfileNotifySink)
|
/// RegisterShellHookWindow does not arrive on Windows 10/11, and TSF notifications
|
||||||
/// сообщают только о смене языка внутри своего процесса — оба варианта проверены
|
/// (ITfLanguageProfileNotifySink) report only language changes inside our own process
|
||||||
/// и не сработали. Один тик — три вызова Win32, читающих данные из памяти ядра.
|
/// — both options were tried and did not work. One tick is three Win32 calls reading
|
||||||
|
/// data from kernel memory.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class KeyboardLayoutService : IKeyboardLayoutService, IDisposable
|
public sealed class KeyboardLayoutService : IKeyboardLayoutService, IDisposable
|
||||||
{
|
{
|
||||||
@@ -75,9 +76,9 @@ public sealed class KeyboardLayoutService : IKeyboardLayoutService, IDisposable
|
|||||||
|
|
||||||
_lastLocaleId = localeId;
|
_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 reason = appSwitched
|
||||||
? LayoutChangeReason.ApplicationSwitched
|
? LayoutChangeReason.ApplicationSwitched
|
||||||
: LayoutChangeReason.UserSwitched;
|
: LayoutChangeReason.UserSwitched;
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ using CursorLang.Models;
|
|||||||
namespace CursorLang.Services;
|
namespace CursorLang.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Связывает слежение за раскладкой с показом подсказки.
|
/// Ties layout tracking to showing the popup.
|
||||||
/// Живёт всё время работы приложения независимо от открытых окон.
|
/// Lives for as long as the application runs, regardless of the open windows.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class LayoutNotificationCoordinator : IDisposable
|
public sealed class LayoutNotificationCoordinator : IDisposable
|
||||||
{
|
{
|
||||||
@@ -31,8 +31,8 @@ public sealed class LayoutNotificationCoordinator : IDisposable
|
|||||||
|
|
||||||
private void OnLayoutChanged(object? sender, LayoutChangedEventArgs e)
|
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)
|
if (e.Reason == LayoutChangeReason.UserSwitched)
|
||||||
{
|
{
|
||||||
_popupService.Show(e.Layout);
|
_popupService.Show(e.Layout);
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ using CursorLang.Views;
|
|||||||
namespace CursorLang.Services;
|
namespace CursorLang.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Управляет временем жизни подсказки: окно отвечает только за показ,
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class LayoutPopupService : ILayoutPopupService, IDisposable
|
public sealed class LayoutPopupService : ILayoutPopupService, IDisposable
|
||||||
{
|
{
|
||||||
@@ -29,8 +29,8 @@ public sealed class LayoutPopupService : ILayoutPopupService, IDisposable
|
|||||||
{
|
{
|
||||||
ShowUntilHidden(layout);
|
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.Interval = _settings.Duration;
|
||||||
_hideTimer.Start();
|
_hideTimer.Start();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
namespace CursorLang.Services;
|
namespace CursorLang.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Берёт строки из ресурсов и при смене языка просит WPF перечитать все привязки.
|
/// Takes the strings from the resources and, when the language changes, asks WPF to
|
||||||
|
/// re-read every binding.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class LocalizationService : ObservableObject, ILocalizationService
|
public sealed class LocalizationService : ObservableObject, ILocalizationService
|
||||||
{
|
{
|
||||||
@@ -38,8 +39,8 @@ public sealed class LocalizationService : ObservableObject, ILocalizationService
|
|||||||
|
|
||||||
OnPropertyChanged(nameof(CurrentLanguage));
|
OnPropertyChanged(nameof(CurrentLanguage));
|
||||||
|
|
||||||
// Сообщаем об изменении индексатора: так обновляются все привязки
|
// We report a change of the indexer: that is how every binding of the
|
||||||
// вида {Binding Localization[Key]}, то есть весь текст интерфейса
|
// {Binding Localization[Key]} kind updates, that is, all the interface text
|
||||||
OnPropertyChanged(Binding.IndexerName);
|
OnPropertyChanged(Binding.IndexerName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,25 +5,26 @@ using CursorLang.Interop;
|
|||||||
namespace CursorLang.Services;
|
namespace CursorLang.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Решает, где появиться окну настроек: впервые за сеанс — по центру монитора,
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Положение живёт только в памяти и между запусками не сохраняется: набор
|
/// 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.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class MainWindowPlacement
|
public sealed class MainWindowPlacement
|
||||||
{
|
{
|
||||||
private PopupWindowNative.Point? _position;
|
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;
|
private bool _isPlacing;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Берёт на себя размещение окна: ставит его на место к первому показу
|
/// Takes over the placement of the window: puts it in place by the first show
|
||||||
/// и следит за тем, куда пользователь его переносит.
|
/// and follows where the user moves it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Attach(Window window)
|
public void Attach(Window window)
|
||||||
{
|
{
|
||||||
@@ -32,13 +33,13 @@ public sealed class MainWindowPlacement
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Возвращает окно на запомненное место, а если за этот сеанс его ещё
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Apply(Window window)
|
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)
|
if (window.WindowState != WindowState.Normal)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -69,10 +70,10 @@ public sealed class MainWindowPlacement
|
|||||||
_position = target;
|
_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)
|
private void OnSourceInitialized(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is not Window window)
|
if (sender is not Window window)
|
||||||
@@ -115,15 +116,15 @@ public sealed class MainWindowPlacement
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Подтягивает окно в рабочую область ближайшего монитора.
|
/// Pulls the window into the work area of the nearest monitor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Нужно в двух случаях. Монитор, на который окно поставил пользователь,
|
/// 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.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
private static PopupWindowNative.Point? KeepOnScreen(
|
private static PopupWindowNative.Point? KeepOnScreen(
|
||||||
PopupWindowNative.Point position, PopupWindowNative.Rect bounds)
|
PopupWindowNative.Point position, PopupWindowNative.Rect bounds)
|
||||||
|
|||||||
@@ -9,8 +9,20 @@ using CursorLang.Models;
|
|||||||
namespace CursorLang.Services;
|
namespace CursorLang.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Хранит настройки в %APPDATA%\CursorLang\settings.json.
|
/// Keeps the settings in the settings.json file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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.
|
||||||
|
/// </remarks>
|
||||||
public sealed class SettingsService : IDisposable
|
public sealed class SettingsService : IDisposable
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||||
@@ -37,8 +49,8 @@ public sealed class SettingsService : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Читает настройки с диска либо отдаёт значения по умолчанию,
|
/// Reads the settings from disk or returns the default values,
|
||||||
/// и дальше сам сохраняет любые изменения.
|
/// and from then on saves any changes by itself.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public AppSettings Load()
|
public AppSettings Load()
|
||||||
{
|
{
|
||||||
@@ -61,7 +73,7 @@ public sealed class SettingsService : IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception e) when (e is IOException or UnauthorizedAccessException)
|
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();
|
Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Color не сериализуется штатно, а хранить его читаемым в файле удобно
|
// Color is not serialized out of the box, and keeping it readable in the file is handy
|
||||||
private sealed class ColorJsonConverter : JsonConverter<Color>
|
private sealed class ColorJsonConverter : JsonConverter<Color>
|
||||||
{
|
{
|
||||||
public override Color Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override Color Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ using CursorLang.Interop;
|
|||||||
namespace CursorLang.Services;
|
namespace CursorLang.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Пускает работать только один экземпляр приложения: повторный запуск не
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Имена объектов ядра оставлены без префикса 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.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class SingleInstanceGate : IDisposable
|
public sealed class SingleInstanceGate : IDisposable
|
||||||
{
|
{
|
||||||
@@ -29,8 +30,8 @@ public sealed class SingleInstanceGate : IDisposable
|
|||||||
public event EventHandler? ActivationRequested;
|
public event EventHandler? ActivationRequested;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Занимает место единственного экземпляра. Если приложение уже работает,
|
/// Takes the single-instance slot. When the application is already running, asks
|
||||||
/// просит его показаться и возвращает <c>false</c> — вызвавшему остаётся выйти.
|
/// it to show itself and returns <c>false</c> — the caller is left to exit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool TryAcquire()
|
public bool TryAcquire()
|
||||||
{
|
{
|
||||||
@@ -42,15 +43,15 @@ public sealed class SingleInstanceGate : IDisposable
|
|||||||
}
|
}
|
||||||
catch (AbandonedMutexException)
|
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;
|
_isOwner = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Событие открывают оба экземпляра: первый — чтобы ждать просьбы,
|
// 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);
|
_activationRequest = new EventWaitHandle(false, EventResetMode.AutoReset, _activationEventName);
|
||||||
|
|
||||||
if (!_isOwner)
|
if (!_isOwner)
|
||||||
{
|
{
|
||||||
@@ -59,8 +60,8 @@ public sealed class SingleInstanceGate : IDisposable
|
|||||||
return false;
|
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(
|
_activationWait = ThreadPool.RegisterWaitForSingleObject(
|
||||||
_activationRequest,
|
_activationRequest,
|
||||||
OnActivationSignalled,
|
OnActivationSignalled,
|
||||||
@@ -79,8 +80,8 @@ public sealed class SingleInstanceGate : IDisposable
|
|||||||
_activationRequest?.Dispose();
|
_activationRequest?.Dispose();
|
||||||
_activationRequest = null;
|
_activationRequest = null;
|
||||||
|
|
||||||
// Мьютекс отпускает тот же поток, что его занял: и то и другое
|
// The mutex is released by the same thread that took it: both happen
|
||||||
// происходит на потоке пользовательского интерфейса
|
// on the user interface thread
|
||||||
if (_isOwner)
|
if (_isOwner)
|
||||||
{
|
{
|
||||||
_mutex?.ReleaseMutex();
|
_mutex?.ReleaseMutex();
|
||||||
@@ -91,8 +92,8 @@ public sealed class SingleInstanceGate : IDisposable
|
|||||||
_mutex = null;
|
_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) =>
|
private void OnActivationSignalled(object? state, bool timedOut) =>
|
||||||
_dispatcher.BeginInvoke(() => ActivationRequested?.Invoke(this, EventArgs.Empty));
|
_dispatcher.BeginInvoke(() => ActivationRequested?.Invoke(this, EventArgs.Empty));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ using Microsoft.Win32;
|
|||||||
namespace CursorLang.Services;
|
namespace CursorLang.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Держит в ресурсах приложения палитру выбранной темы и подменяет её
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ThemeService : IThemeService, IDisposable
|
public sealed class ThemeService : IThemeService, IDisposable
|
||||||
{
|
{
|
||||||
@@ -29,7 +29,7 @@ public sealed class ThemeService : IThemeService, IDisposable
|
|||||||
Apply();
|
Apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Тема, которую видит пользователь: <c>System</c> здесь уже разрешён.</summary>
|
/// <summary>The theme the user sees: <c>System</c> is already resolved here.</summary>
|
||||||
public AppTheme CurrentTheme => _current;
|
public AppTheme CurrentTheme => _current;
|
||||||
|
|
||||||
public void Register(Window window)
|
public void Register(Window window)
|
||||||
@@ -60,8 +60,8 @@ public sealed class ThemeService : IThemeService, IDisposable
|
|||||||
_windows.Clear();
|
_windows.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Тема приложений в настройках Windows.</summary>
|
/// <summary>The app theme from the Windows settings.</summary>
|
||||||
private static AppTheme DetectSystemTheme()
|
internal static AppTheme DetectSystemTheme()
|
||||||
{
|
{
|
||||||
try
|
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) =>
|
private void OnUserPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e) =>
|
||||||
Application.Current?.Dispatcher.InvokeAsync(Apply);
|
Application.Current?.Dispatcher.InvokeAsync(Apply);
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ using CursorLang.Models;
|
|||||||
namespace CursorLang.ViewModels;
|
namespace CursorLang.ViewModels;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Содержимое подсказки у курсора. Внешний вид берётся прямо из настроек,
|
/// The content of the popup at the cursor. The look is taken straight from the
|
||||||
/// поэтому их правка применяется без перезапуска.
|
/// settings, so editing them applies without a restart.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class LayoutPopupViewModel : ObservableObject
|
public sealed partial class LayoutPopupViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ using CursorLang.Services;
|
|||||||
namespace CursorLang.ViewModels;
|
namespace CursorLang.ViewModels;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Вариант выбора в списке. Подпись меняется вместе с языком, а сам объект
|
/// 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
|
||||||
/// ComboBox сбрасывает выбранное значение.
|
/// makes the ComboBox drop the selected value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class EnumOption<T> : ObservableObject where T : struct, Enum
|
public sealed class EnumOption<T> : ObservableObject where T : struct, Enum
|
||||||
{
|
{
|
||||||
@@ -30,13 +30,13 @@ public sealed class EnumOption<T> : ObservableObject where T : struct, Enum
|
|||||||
set => SetProperty(ref _display, value);
|
set => SetProperty(ref _display, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Средства доступности берут имя элемента списка отсюда
|
// Accessibility tools take the name of the list item from here
|
||||||
public override string ToString() => Display;
|
public override string ToString() => Display;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Окно настроек. Значения правятся прямо в <see cref="AppSettings"/>,
|
/// The settings window. The values are edited right in <see cref="AppSettings"/>,
|
||||||
/// поэтому подсказка подхватывает их сразу, без кнопки «Применить».
|
/// so the popup picks them up at once, without an "Apply" button.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class SettingsViewModel : ObservableObject, IDisposable
|
public sealed partial class SettingsViewModel : ObservableObject, IDisposable
|
||||||
{
|
{
|
||||||
@@ -69,7 +69,7 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable
|
|||||||
Settings = settings;
|
Settings = settings;
|
||||||
Localization = localization;
|
Localization = localization;
|
||||||
|
|
||||||
// Язык интерфейса — такая же настройка, как остальные, и хранится там же
|
// The interface language is a setting like any other and is stored in the same place
|
||||||
Localization.CurrentLanguage = settings.Language;
|
Localization.CurrentLanguage = settings.Language;
|
||||||
Settings.PropertyChanged += OnSettingsChanged;
|
Settings.PropertyChanged += OnSettingsChanged;
|
||||||
Localization.PropertyChanged += OnLocalizationChanged;
|
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)
|
private void OnLocalizationChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.PropertyName != Binding.IndexerName)
|
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>(T value) where T : struct, Enum =>
|
private string GetDisplayName<T>(T value) where T : struct, Enum =>
|
||||||
Localization[$"{typeof(T).Name}_{value}"];
|
Localization[$"{typeof(T).Name}_{value}"];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ using System.Windows.Media;
|
|||||||
namespace CursorLang.Views;
|
namespace CursorLang.Views;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Показывает элемент, если значение совпадает с одним из перечисленных
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class EnumToVisibilityConverter : IValueConverter
|
public sealed class EnumToVisibilityConverter : IValueConverter
|
||||||
{
|
{
|
||||||
@@ -27,8 +27,8 @@ public sealed class EnumToVisibilityConverter : IValueConverter
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Цвет в запись вида «#RRGGBB»: альфа не показывается, потому что
|
/// A colour into a "#RRGGBB" notation: the alpha is not shown, because
|
||||||
/// за прозрачность отвечает отдельная настройка.
|
/// transparency is a separate setting.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ColorToHexConverter : IValueConverter
|
public sealed class ColorToHexConverter : IValueConverter
|
||||||
{
|
{
|
||||||
@@ -40,7 +40,7 @@ public sealed class ColorToHexConverter : IValueConverter
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Цвет в кисть — для образцов палитры в списке.
|
/// A colour into a brush — for the palette swatches in the list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ColorToBrushConverter : IValueConverter
|
public sealed class ColorToBrushConverter : IValueConverter
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ using CursorLang.ViewModels;
|
|||||||
namespace CursorLang.Views;
|
namespace CursorLang.Views;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Всплывающая подсказка с коротким именем раскладки.
|
/// 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
|
||||||
/// решает <see cref="Services.LayoutPopupService"/>.
|
/// down is decided by <see cref="Services.LayoutPopupService"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class LayoutPopupWindow : Window
|
public partial class LayoutPopupWindow : Window
|
||||||
{
|
{
|
||||||
@@ -23,25 +23,25 @@ public partial class LayoutPopupWindow : Window
|
|||||||
DataContext = viewModel;
|
DataContext = viewModel;
|
||||||
_settings = settings;
|
_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();
|
new WindowInteropHelper(this).EnsureHandle();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Показывает подсказку в месте, заданном настройками.
|
/// Shows the popup at the place set by the settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ShowPopup()
|
public void ShowPopup()
|
||||||
{
|
{
|
||||||
MeasureContent();
|
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();
|
ApplyBounds();
|
||||||
Show();
|
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();
|
ApplyBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,9 +52,9 @@ public partial class LayoutPopupWindow : Window
|
|||||||
PopupWindowNative.MakePassive(new WindowInteropHelper(this).Handle);
|
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()
|
private void MeasureContent()
|
||||||
{
|
{
|
||||||
var content = (FrameworkElement)Content;
|
var content = (FrameworkElement)Content;
|
||||||
@@ -63,9 +63,9 @@ public partial class LayoutPopupWindow : Window
|
|||||||
_contentSize = content.DesiredSize;
|
_contentSize = content.DesiredSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Всё считается в физических пикселях: у мониторов разный масштаб, а
|
// Everything is computed in physical pixels: monitors have different scaling,
|
||||||
// Window.Left/Top/Width/Height пересчитываются по DPI того монитора,
|
// 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()
|
private void ApplyBounds()
|
||||||
{
|
{
|
||||||
IntPtr handle = new WindowInteropHelper(this).Handle;
|
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()
|
private PopupWindowNative.Rect GetAnchor()
|
||||||
{
|
{
|
||||||
if (_settings.PlacementMode == PopupPlacementMode.AtCaret &&
|
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 };
|
var anchorPoint = new PopupWindowNative.Point { X = anchor.Left, Y = anchor.Top };
|
||||||
double scale = PopupWindowNative.GetScaleAt(anchorPoint);
|
double scale = PopupWindowNative.GetScaleAt(anchorPoint);
|
||||||
|
|
||||||
// У каждого режима привязки своя сторона и свой отступ
|
// Every anchor mode has a side and an offset of its own
|
||||||
bool atCaret = _settings.PlacementMode == PopupPlacementMode.AtCaret;
|
bool atCaret = _settings.PlacementMode == PopupPlacementMode.AtCaret;
|
||||||
AnchorSide side = atCaret ? _settings.CaretSide : _settings.CursorSide;
|
AnchorSide side = atCaret ? _settings.CaretSide : _settings.CursorSide;
|
||||||
int offset = ToPixels(atCaret ? _settings.CaretOffset : _settings.CursorOffset, scale);
|
int offset = ToPixels(atCaret ? _settings.CaretOffset : _settings.CursorOffset, scale);
|
||||||
|
|||||||
@@ -35,6 +35,9 @@
|
|||||||
<Style TargetType="TextBlock" x:Key="FieldLabel">
|
<Style TargetType="TextBlock" x:Key="FieldLabel">
|
||||||
<Setter Property="VerticalAlignment" Value="Center" />
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
<Setter Property="Margin" Value="0,0,12,0" />
|
<Setter Property="Margin" Value="0,0,12,0" />
|
||||||
|
<!-- The caption column is narrow, and a translation may not fit into it:
|
||||||
|
such a caption is better wrapped onto a second line than cut off -->
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style TargetType="TextBlock" x:Key="FieldValue">
|
<Style TargetType="TextBlock" x:Key="FieldValue">
|
||||||
<Setter Property="VerticalAlignment" Value="Center" />
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
@@ -45,6 +48,9 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</Window.Resources>
|
</Window.Resources>
|
||||||
|
|
||||||
|
<!-- The settings are laid out in two columns: this way the window fits on the
|
||||||
|
screen entirely and does without scrolling. Scrolling is kept for the case of
|
||||||
|
a large system font, with which the content is taller than the monitor after all -->
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="16">
|
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using CursorLang.ViewModels;
|
|||||||
namespace CursorLang.Views;
|
namespace CursorLang.Views;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Окно настроек приложения.
|
/// The settings window of the application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user