fixed comment into code
This commit is contained in:
@@ -5,13 +5,13 @@ using CursorLang.Models;
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Держит системный перехват Caps Lock и отличает короткое нажатие от удержания.
|
||||
/// Holds the system Caps Lock hook and tells a short tap from a hold.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Различить их можно только по факту отпускания клавиши, поэтому перехватываются
|
||||
/// оба события — и нажатие, и отпускание. Заодно это единственный способ отменить
|
||||
/// смену регистра: Windows переключает Caps Lock по событию нажатия, и пропустить
|
||||
/// его «на всякий случай» нельзя.
|
||||
/// They can be told apart only by the key being released, so both events are
|
||||
/// intercepted — the press and the release. That is also the only way to cancel the
|
||||
/// case change: Windows toggles Caps Lock on the press event, and letting it through
|
||||
/// "just in case" is not an option.
|
||||
/// </remarks>
|
||||
public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable
|
||||
{
|
||||
@@ -28,7 +28,7 @@ public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable
|
||||
public CapsLockHotkeyService(AppSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
_hook = new LowLevelKeyboardHook(OnKeyEvent);
|
||||
_hook = new LowLevelKeyboardHook(HandleKeyEvent);
|
||||
_holdTimer.Tick += OnHoldTimerTick;
|
||||
}
|
||||
|
||||
@@ -48,9 +48,9 @@ public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable
|
||||
ResetPress();
|
||||
}
|
||||
|
||||
// При закрытии приложения событий уже никто не ждёт, поэтому в отличие
|
||||
// от Stop состояние сбрасывается молча: очередь диспетчера в этот момент
|
||||
// может быть закрыта
|
||||
// When the application is closing, nobody is waiting for events any more, so
|
||||
// unlike in Stop the state is reset quietly: the dispatcher queue may be shut
|
||||
// down by that moment
|
||||
public void Dispose()
|
||||
{
|
||||
_holdTimer.Tick -= OnHoldTimerTick;
|
||||
@@ -60,10 +60,12 @@ public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable
|
||||
_hook.Dispose();
|
||||
}
|
||||
|
||||
// Вызывается системным хуком, то есть внутри разбора очереди сообщений.
|
||||
// Здесь только учёт состояния: показывать окна и рассылать события отсюда
|
||||
// нельзя — обработчик обязан вернуть управление за считанные миллисекунды
|
||||
private bool OnKeyEvent(int virtualKey, bool isKeyDown)
|
||||
// Called by the system hook, that is, inside message queue processing. Only state
|
||||
// tracking belongs here: showing windows and raising events from here is not
|
||||
// allowed — the handler must return control within a few milliseconds.
|
||||
// In tests the key presses are fed here as well: there is no need to install a
|
||||
// real keyboard hook just to check how presses are interpreted
|
||||
internal bool HandleKeyEvent(int virtualKey, bool isKeyDown)
|
||||
{
|
||||
if (virtualKey != VirtualKeyCapsLock)
|
||||
{
|
||||
@@ -72,13 +74,13 @@ public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable
|
||||
|
||||
if (isKeyDown)
|
||||
{
|
||||
// Пока клавишу держат, Windows повторяет нажатие: отсчёт удержания
|
||||
// ведём от первого события и на повторы не реагируем
|
||||
// While the key is held, Windows repeats the press: the hold is counted
|
||||
// from the first event and the repeats are ignored
|
||||
if (!_isPressed)
|
||||
{
|
||||
_isPressed = true;
|
||||
|
||||
// Порог читаем при каждом нажатии: его меняют в настройках на лету
|
||||
// The threshold is read on every press: it is changed in the settings on the fly
|
||||
_holdTimer.Interval = _settings.CapsLockHoldDelay;
|
||||
_holdTimer.Start();
|
||||
}
|
||||
@@ -109,8 +111,8 @@ public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable
|
||||
HoldStarted?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
// Событие уходит подписчикам после возврата из хука: они вправе показывать
|
||||
// окна и делать что угодно ещё, не задерживая обработку нажатия
|
||||
// The event reaches the subscribers after the hook returns: they are free to show
|
||||
// windows and do anything else without holding up the handling of the key press
|
||||
private void Notify(EventHandler? handler)
|
||||
{
|
||||
if (handler is not null)
|
||||
@@ -119,8 +121,8 @@ public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// Перехват могли снять с зажатой клавишей — например, сняв галочку
|
||||
// в настройках. Подсказку в этом случае нужно убрать
|
||||
// The hook may have been removed with the key held down — by clearing the
|
||||
// checkbox in the settings, for instance. The popup has to be taken down then
|
||||
private void ResetPress()
|
||||
{
|
||||
_isPressed = false;
|
||||
|
||||
@@ -4,9 +4,9 @@ using CursorLang.Models;
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <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>
|
||||
public sealed class CapsLockSwitchCoordinator : IDisposable
|
||||
{
|
||||
@@ -68,8 +68,8 @@ public sealed class CapsLockSwitchCoordinator : IDisposable
|
||||
|
||||
private void OnTapped(object? sender, EventArgs e) => _layoutService.SwitchToNext();
|
||||
|
||||
// Раскладку не меняем, но и молчать нельзя: без подсказки долгое нажатие
|
||||
// выглядит как то, что клавиша просто не сработала
|
||||
// We do not change the layout, but staying silent will not do either: without the
|
||||
// popup a long press looks as if the key simply did not work
|
||||
private void OnHoldStarted(object? sender, EventArgs e) =>
|
||||
_popupService.ShowUntilHidden(_layoutService.Current);
|
||||
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <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>
|
||||
public interface ICapsLockHotkeyService
|
||||
{
|
||||
/// <summary>Короткое нажатие: клавишу отпустили до порога удержания.</summary>
|
||||
/// <summary>A short press: the key was released before the hold threshold.</summary>
|
||||
event EventHandler? Tapped;
|
||||
|
||||
/// <summary>Порог удержания пройден, клавишу всё ещё держат.</summary>
|
||||
/// <summary>The hold threshold has passed, the key is still held.</summary>
|
||||
event EventHandler? HoldStarted;
|
||||
|
||||
/// <summary>Удержание закончилось: клавишу отпустили.</summary>
|
||||
/// <summary>The hold is over: the key was released.</summary>
|
||||
event EventHandler? HoldEnded;
|
||||
|
||||
/// <summary>Стоит ли перехват прямо сейчас.</summary>
|
||||
/// <summary>Whether the hook is installed right now.</summary>
|
||||
bool IsRunning { get; }
|
||||
|
||||
/// <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>
|
||||
void Start();
|
||||
|
||||
/// <summary>Снимает перехват, возвращая клавише её обычное поведение.</summary>
|
||||
/// <summary>Removes the hook, giving the key its usual behaviour back.</summary>
|
||||
void Stop();
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ using CursorLang.Models;
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Следит за раскладкой активного окна — в том числе в чужих приложениях —
|
||||
/// и умеет её переключать.
|
||||
/// Tracks the layout of the active window — including in other applications —
|
||||
/// and can switch it.
|
||||
/// </summary>
|
||||
public interface IKeyboardLayoutService
|
||||
{
|
||||
/// <summary>Раскладка активного окна на текущий момент.</summary>
|
||||
/// <summary>The layout of the active window at the moment.</summary>
|
||||
KeyboardLayout Current { get; }
|
||||
|
||||
event EventHandler<LayoutChangedEventArgs>? LayoutChanged;
|
||||
|
||||
@@ -3,16 +3,16 @@ using CursorLang.Models;
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Показывает подсказку с раскладкой у курсора.
|
||||
/// Shows the layout popup at the cursor.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <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>
|
||||
void ShowUntilHidden(KeyboardLayout layout);
|
||||
|
||||
|
||||
@@ -2,21 +2,21 @@ using System.ComponentModel;
|
||||
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>Язык интерфейса для выбора в настройках.</summary>
|
||||
/// <param name="Code">Код культуры: «ru», «en».</param>
|
||||
/// <param name="DisplayName">Название на самом этом языке.</param>
|
||||
/// <summary>An interface language to choose from in the settings.</summary>
|
||||
/// <param name="Code">The culture code: "ru", "en".</param>
|
||||
/// <param name="DisplayName">The name in that very language.</param>
|
||||
public sealed record LanguageOption(string Code, string DisplayName)
|
||||
{
|
||||
// Средства доступности берут имя элемента списка отсюда
|
||||
// Accessibility tools take the name of the list item from here
|
||||
public override string ToString() => DisplayName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Даёт строки интерфейса и умеет менять язык без перезапуска.
|
||||
/// Provides the interface strings and can change the language without a restart.
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
IReadOnlyList<LanguageOption> AvailableLanguages { get; }
|
||||
|
||||
@@ -4,16 +4,16 @@ using CursorLang.Models;
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Применяет светлое или тёмное оформление к окнам приложения.
|
||||
/// Applies the light or the dark look to the windows of the application.
|
||||
/// </summary>
|
||||
public interface IThemeService
|
||||
{
|
||||
/// <summary>Тема, действующая сейчас.</summary>
|
||||
/// <summary>The theme in effect right now.</summary>
|
||||
AppTheme CurrentTheme { get; }
|
||||
|
||||
/// <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>
|
||||
void Register(Window window);
|
||||
}
|
||||
|
||||
@@ -5,23 +5,24 @@ using CursorLang.Models;
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Настройки слежения за раскладкой.
|
||||
/// The settings of layout tracking.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Опрашивает активное окно по таймеру.
|
||||
/// Polls the active window on a timer.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Опрос выбран не от простоты: событийных способов узнать о смене раскладки
|
||||
/// в чужом процессе из managed-кода нет. HSHELL_LANGUAGE от RegisterShellHookWindow
|
||||
/// в Windows 10/11 не приходит, а уведомления TSF (ITfLanguageProfileNotifySink)
|
||||
/// сообщают только о смене языка внутри своего процесса — оба варианта проверены
|
||||
/// и не сработали. Один тик — три вызова Win32, читающих данные из памяти ядра.
|
||||
/// Polling was chosen not for simplicity: there is no event-based way to learn about
|
||||
/// a layout change in another process from managed code. HSHELL_LANGUAGE from
|
||||
/// RegisterShellHookWindow does not arrive on Windows 10/11, and TSF notifications
|
||||
/// (ITfLanguageProfileNotifySink) report only language changes inside our own process
|
||||
/// — both options were tried and did not work. One tick is three Win32 calls reading
|
||||
/// data from kernel memory.
|
||||
/// </remarks>
|
||||
public sealed class KeyboardLayoutService : IKeyboardLayoutService, IDisposable
|
||||
{
|
||||
@@ -75,9 +76,9 @@ public sealed class KeyboardLayoutService : IKeyboardLayoutService, IDisposable
|
||||
|
||||
_lastLocaleId = localeId;
|
||||
|
||||
// Переход в другое приложение со своей раскладкой — не то же самое,
|
||||
// что переключение раскладки пользователем, и подписчики вправе
|
||||
// реагировать на эти случаи по-разному
|
||||
// Moving to another application with a layout of its own is not the same as
|
||||
// the user switching the layout, and the subscribers are free to react to
|
||||
// these cases differently
|
||||
LayoutChangeReason reason = appSwitched
|
||||
? LayoutChangeReason.ApplicationSwitched
|
||||
: LayoutChangeReason.UserSwitched;
|
||||
|
||||
@@ -3,8 +3,8 @@ using CursorLang.Models;
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Связывает слежение за раскладкой с показом подсказки.
|
||||
/// Живёт всё время работы приложения независимо от открытых окон.
|
||||
/// Ties layout tracking to showing the popup.
|
||||
/// Lives for as long as the application runs, regardless of the open windows.
|
||||
/// </summary>
|
||||
public sealed class LayoutNotificationCoordinator : IDisposable
|
||||
{
|
||||
@@ -31,8 +31,8 @@ public sealed class LayoutNotificationCoordinator : IDisposable
|
||||
|
||||
private void OnLayoutChanged(object? sender, LayoutChangedEventArgs e)
|
||||
{
|
||||
// При переходе в другое приложение раскладка меняется без участия
|
||||
// пользователя, и всплывающая подсказка была бы навязчивой
|
||||
// When moving to another application the layout changes without the user
|
||||
// taking part, and a popup would be intrusive
|
||||
if (e.Reason == LayoutChangeReason.UserSwitched)
|
||||
{
|
||||
_popupService.Show(e.Layout);
|
||||
|
||||
@@ -6,8 +6,8 @@ using CursorLang.Views;
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <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>
|
||||
public sealed class LayoutPopupService : ILayoutPopupService, IDisposable
|
||||
{
|
||||
@@ -29,8 +29,8 @@ public sealed class LayoutPopupService : ILayoutPopupService, IDisposable
|
||||
{
|
||||
ShowUntilHidden(layout);
|
||||
|
||||
// Длительность читаем при каждом показе: её меняют в настройках на лету.
|
||||
// Перезапуск таймера заодно продлевает показ при быстрых переключениях
|
||||
// The duration is read on every show: it is changed in the settings on the fly.
|
||||
// Restarting the timer also prolongs the show on quick switches
|
||||
_hideTimer.Interval = _settings.Duration;
|
||||
_hideTimer.Start();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Берёт строки из ресурсов и при смене языка просит WPF перечитать все привязки.
|
||||
/// Takes the strings from the resources and, when the language changes, asks WPF to
|
||||
/// re-read every binding.
|
||||
/// </summary>
|
||||
public sealed class LocalizationService : ObservableObject, ILocalizationService
|
||||
{
|
||||
@@ -38,8 +39,8 @@ public sealed class LocalizationService : ObservableObject, ILocalizationService
|
||||
|
||||
OnPropertyChanged(nameof(CurrentLanguage));
|
||||
|
||||
// Сообщаем об изменении индексатора: так обновляются все привязки
|
||||
// вида {Binding Localization[Key]}, то есть весь текст интерфейса
|
||||
// We report a change of the indexer: that is how every binding of the
|
||||
// {Binding Localization[Key]} kind updates, that is, all the interface text
|
||||
OnPropertyChanged(Binding.IndexerName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,25 +5,26 @@ using CursorLang.Interop;
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <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>
|
||||
/// <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>
|
||||
public sealed class MainWindowPlacement
|
||||
{
|
||||
private PopupWindowNative.Point? _position;
|
||||
|
||||
// Окно сообщает о переносе и тогда, когда двигаем его мы сами;
|
||||
// запоминать надо только то, что выбрал пользователь
|
||||
// The window reports a move when we move it ourselves as well;
|
||||
// what has to be remembered is only what the user chose
|
||||
private bool _isPlacing;
|
||||
|
||||
/// <summary>
|
||||
/// Берёт на себя размещение окна: ставит его на место к первому показу
|
||||
/// и следит за тем, куда пользователь его переносит.
|
||||
/// Takes over the placement of the window: puts it in place by the first show
|
||||
/// and follows where the user moves it.
|
||||
/// </summary>
|
||||
public void Attach(Window window)
|
||||
{
|
||||
@@ -32,13 +33,13 @@ public sealed class MainWindowPlacement
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public void Apply(Window window)
|
||||
{
|
||||
// У свёрнутого окна нет осмысленных границ: его разворачивают на прежнем
|
||||
// месте, и разместить его можно только после этого
|
||||
// A minimized window has no meaningful bounds: it is restored in its former
|
||||
// place, and it can be positioned only after that
|
||||
if (window.WindowState != WindowState.Normal)
|
||||
{
|
||||
return;
|
||||
@@ -69,10 +70,10 @@ public sealed class MainWindowPlacement
|
||||
_position = target;
|
||||
}
|
||||
|
||||
// Высота окна подстраивается под содержимое, и до первой раскладки она
|
||||
// неизвестна — по центру встал бы пустой каркас окна. Раскладку поэтому
|
||||
// просим посчитать сразу: окно к этому моменту ещё не показано, так что
|
||||
// на прежнем месте оно не мелькнёт
|
||||
// The window height adapts to its content and is unknown until the first layout
|
||||
// pass — an empty window frame would end up in the centre. So we ask for the
|
||||
// layout to be computed right away: by that moment the window is not shown yet,
|
||||
// so it will not flash in its former place
|
||||
private void OnSourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
if (sender is not Window window)
|
||||
@@ -115,15 +116,15 @@ public sealed class MainWindowPlacement
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Подтягивает окно в рабочую область ближайшего монитора.
|
||||
/// Pulls the window into the work area of the nearest monitor.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
private static PopupWindowNative.Point? KeepOnScreen(
|
||||
PopupWindowNative.Point position, PopupWindowNative.Rect bounds)
|
||||
|
||||
@@ -9,8 +9,20 @@ using CursorLang.Models;
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Хранит настройки в %APPDATA%\CursorLang\settings.json.
|
||||
/// Keeps the settings in the settings.json file.
|
||||
/// </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
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
@@ -37,8 +49,8 @@ public sealed class SettingsService : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Читает настройки с диска либо отдаёт значения по умолчанию,
|
||||
/// и дальше сам сохраняет любые изменения.
|
||||
/// Reads the settings from disk or returns the default values,
|
||||
/// and from then on saves any changes by itself.
|
||||
/// </summary>
|
||||
public AppSettings Load()
|
||||
{
|
||||
@@ -61,7 +73,7 @@ public sealed class SettingsService : IDisposable
|
||||
}
|
||||
catch (Exception e) when (e is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
// Настройки — не тот случай, ради которого стоит ронять приложение
|
||||
// The settings are not the kind of thing worth bringing the application down for
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +124,7 @@ public sealed class SettingsService : IDisposable
|
||||
Save();
|
||||
}
|
||||
|
||||
// Color не сериализуется штатно, а хранить его читаемым в файле удобно
|
||||
// Color is not serialized out of the box, and keeping it readable in the file is handy
|
||||
private sealed class ColorJsonConverter : JsonConverter<Color>
|
||||
{
|
||||
public override Color Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
|
||||
@@ -4,14 +4,15 @@ using CursorLang.Interop;
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <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>
|
||||
/// <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>
|
||||
public sealed class SingleInstanceGate : IDisposable
|
||||
{
|
||||
@@ -29,8 +30,8 @@ public sealed class SingleInstanceGate : IDisposable
|
||||
public event EventHandler? ActivationRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Занимает место единственного экземпляра. Если приложение уже работает,
|
||||
/// просит его показаться и возвращает <c>false</c> — вызвавшему остаётся выйти.
|
||||
/// Takes the single-instance slot. When the application is already running, asks
|
||||
/// it to show itself and returns <c>false</c> — the caller is left to exit.
|
||||
/// </summary>
|
||||
public bool TryAcquire()
|
||||
{
|
||||
@@ -42,15 +43,15 @@ public sealed class SingleInstanceGate : IDisposable
|
||||
}
|
||||
catch (AbandonedMutexException)
|
||||
{
|
||||
// Предыдущий экземпляр завершился аварийно и мьютекс не отпустил.
|
||||
// Владельца у него теперь нет, а значит место свободно
|
||||
// The previous instance crashed and did not release the mutex.
|
||||
// It has no owner now, which means the slot is free
|
||||
_isOwner = true;
|
||||
}
|
||||
|
||||
// Событие открывают оба экземпляра: первый — чтобы ждать просьбы,
|
||||
// второй — чтобы её подать. Кто из них создаст объект, зависит от того,
|
||||
// кто оказался первым, и на работу не влияет
|
||||
_activationRequest = new EventWaitHandle(false, EventResetMode.AutoReset, ActivationEventName);
|
||||
// The event is opened by both instances: the first one to wait for a request,
|
||||
// the second one to make it. Which of them creates the object depends on who
|
||||
// came first and does not affect the work
|
||||
_activationRequest = new EventWaitHandle(false, EventResetMode.AutoReset, _activationEventName);
|
||||
|
||||
if (!_isOwner)
|
||||
{
|
||||
@@ -59,8 +60,8 @@ public sealed class SingleInstanceGate : IDisposable
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ожидание отдано пулу потоков: держать ради него свой поток не за чем,
|
||||
// а просьба может не прийти никогда
|
||||
// The wait is handed over to the thread pool: there is no reason to hold a
|
||||
// thread of our own for it, and the request may never come
|
||||
_activationWait = ThreadPool.RegisterWaitForSingleObject(
|
||||
_activationRequest,
|
||||
OnActivationSignalled,
|
||||
@@ -79,8 +80,8 @@ public sealed class SingleInstanceGate : IDisposable
|
||||
_activationRequest?.Dispose();
|
||||
_activationRequest = null;
|
||||
|
||||
// Мьютекс отпускает тот же поток, что его занял: и то и другое
|
||||
// происходит на потоке пользовательского интерфейса
|
||||
// The mutex is released by the same thread that took it: both happen
|
||||
// on the user interface thread
|
||||
if (_isOwner)
|
||||
{
|
||||
_mutex?.ReleaseMutex();
|
||||
@@ -91,8 +92,8 @@ public sealed class SingleInstanceGate : IDisposable
|
||||
_mutex = null;
|
||||
}
|
||||
|
||||
// Пул потоков сообщает о просьбе где придётся, а окно слушается только
|
||||
// своего потока
|
||||
// The thread pool reports the request from wherever it happens to be, while the
|
||||
// window obeys only its own thread
|
||||
private void OnActivationSignalled(object? state, bool timedOut) =>
|
||||
_dispatcher.BeginInvoke(() => ActivationRequested?.Invoke(this, EventArgs.Empty));
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ using Microsoft.Win32;
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <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>
|
||||
public sealed class ThemeService : IThemeService, IDisposable
|
||||
{
|
||||
@@ -29,7 +29,7 @@ public sealed class ThemeService : IThemeService, IDisposable
|
||||
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 void Register(Window window)
|
||||
@@ -60,8 +60,8 @@ public sealed class ThemeService : IThemeService, IDisposable
|
||||
_windows.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Тема приложений в настройках Windows.</summary>
|
||||
private static AppTheme DetectSystemTheme()
|
||||
/// <summary>The app theme from the Windows settings.</summary>
|
||||
internal static AppTheme DetectSystemTheme()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -84,7 +84,7 @@ public sealed class ThemeService : IThemeService, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// Windows сообщает о смене оформления не из потока интерфейса
|
||||
// Windows reports a theme change from outside the interface thread
|
||||
private void OnUserPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e) =>
|
||||
Application.Current?.Dispatcher.InvokeAsync(Apply);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user