added layour swithing by caps key
This commit is contained in:
@@ -28,12 +28,11 @@ public partial class App : Application
|
||||
MainWindow.Show();
|
||||
|
||||
_services.GetRequiredService<LayoutNotificationCoordinator>().Start();
|
||||
_services.GetRequiredService<CapsLockSwitchCoordinator>().Start();
|
||||
}
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
// Контейнер сам остановит таймеры, сохранит настройки
|
||||
// и закроет окно подсказки
|
||||
_services?.Dispose();
|
||||
base.OnExit(e);
|
||||
}
|
||||
@@ -51,7 +50,9 @@ public partial class App : Application
|
||||
services.AddSingleton<ILocalizationService, LocalizationService>();
|
||||
services.AddSingleton<IKeyboardLayoutService, KeyboardLayoutService>();
|
||||
services.AddSingleton<ILayoutPopupService, LayoutPopupService>();
|
||||
services.AddSingleton<ICapsLockHotkeyService, CapsLockHotkeyService>();
|
||||
services.AddSingleton<LayoutNotificationCoordinator>();
|
||||
services.AddSingleton<CapsLockSwitchCoordinator>();
|
||||
|
||||
services.AddSingleton<LayoutPopupViewModel>();
|
||||
services.AddSingleton<SettingsViewModel>();
|
||||
|
||||
@@ -7,6 +7,14 @@ namespace CursorLang.Interop;
|
||||
/// </summary>
|
||||
internal static class KeyboardLayoutNative
|
||||
{
|
||||
private const uint WmInputLangChangeRequest = 0x0050;
|
||||
|
||||
/// <summary>Взять следующую раскладку из системного списка.</summary>
|
||||
private static readonly IntPtr InputLangChangeForward = new(0x0002);
|
||||
|
||||
/// <summary>HKL_NEXT — та же просьба на языке старых версий Windows.</summary>
|
||||
private static readonly IntPtr HklNext = new(1);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern IntPtr GetForegroundWindow();
|
||||
|
||||
@@ -16,24 +24,53 @@ internal static class KeyboardLayoutNative
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetKeyboardLayout(uint idThread);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
/// <summary>
|
||||
/// Раскладка, которой сейчас печатает пользователь.
|
||||
/// </summary>
|
||||
internal static int GetActiveLocaleId() => GetLocaleIdOf(GetInputWindow());
|
||||
|
||||
/// <summary>
|
||||
/// Просит активное приложение перейти на следующую раскладку из системного списка.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Синтезировать системное сочетание вроде Alt+Shift не годится: его назначение
|
||||
/// пользователь меняет в настройках Windows и может отключить вовсе. Просьба
|
||||
/// сообщением от таких настроек не зависит и работает в чужом процессе.
|
||||
/// </remarks>
|
||||
internal static void RequestNextLayout()
|
||||
{
|
||||
IntPtr target = GetInputWindow();
|
||||
if (target == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Оба параметра означают одно и то же: разные версии Windows и разные
|
||||
// библиотеки интерфейса смотрят то на флаг, то на lParam
|
||||
PostMessage(target, WmInputLangChangeRequest, InputLangChangeForward, HklNext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Окно, которому принадлежит ввод с клавиатуры.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Спрашиваем окно с фокусом клавиатуры, а не окно верхнего плана: у Блокнота
|
||||
/// Windows 11, меню «Пуск» и прочих приложений на WinUI поле ввода живёт в
|
||||
/// отдельном потоке, и раскладка меняется только у него. У потока главного
|
||||
/// окна она остаётся прежней, и переключение проходит незамеченным.
|
||||
/// </remarks>
|
||||
internal static int GetActiveLocaleId()
|
||||
private static IntPtr GetInputWindow()
|
||||
{
|
||||
if (ForegroundInputNative.TryGetInfo(out ForegroundInputNative.GuiThreadInfo info) &&
|
||||
info.hwndFocus != IntPtr.Zero)
|
||||
{
|
||||
return GetLocaleIdOf(info.hwndFocus);
|
||||
return info.hwndFocus;
|
||||
}
|
||||
|
||||
return GetLocaleIdOf(GetForegroundWindow());
|
||||
return GetForegroundWindow();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace CursorLang.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Системный перехватчик клавиатуры (WH_KEYBOARD_LL): видит нажатия во всех
|
||||
/// приложениях и умеет не пропускать их дальше.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Ставится только низкоуровневый хук: обычный WH_KEYBOARD требует внедрения
|
||||
/// DLL в чужие процессы, что для managed-кода невозможно. Обратный вызов
|
||||
/// приходит на тот поток, который поставил хук, и поток обязан крутить цикл
|
||||
/// сообщений — отсюда требование ставить хук из потока пользовательского
|
||||
/// интерфейса. Возврат управления затягивать нельзя: по истечении системного
|
||||
/// тайм-аута Windows молча снимает хук.
|
||||
/// </remarks>
|
||||
internal sealed class LowLevelKeyboardHook : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Обработчик события клавиши. Возвращает <c>true</c>, если событие нужно
|
||||
/// проглотить — тогда приложение переднего плана его не увидит.
|
||||
/// </summary>
|
||||
internal delegate bool KeyFilter(int virtualKey, bool isKeyDown);
|
||||
|
||||
private const int WhKeyboardLowLevel = 13;
|
||||
private const int HcAction = 0;
|
||||
|
||||
private const int WmKeyDown = 0x0100;
|
||||
private const int WmKeyUp = 0x0101;
|
||||
private const int WmSysKeyDown = 0x0104;
|
||||
private const int WmSysKeyUp = 0x0105;
|
||||
|
||||
/// <summary>Событие пришло не от живого нажатия, а из SendInput.</summary>
|
||||
private const uint LowLevelKeyHookFlagInjected = 0x10;
|
||||
|
||||
private readonly KeyFilter _filter;
|
||||
|
||||
// Делегат живёт в поле не для удобства: ссылку на него держит только Win32,
|
||||
// о которой сборщик мусора не знает, и без поля хук перестаёт работать
|
||||
// через случайное время
|
||||
private readonly HookProc _callback;
|
||||
|
||||
private IntPtr _handle;
|
||||
|
||||
internal LowLevelKeyboardHook(KeyFilter filter)
|
||||
{
|
||||
_filter = filter;
|
||||
_callback = OnHookEvent;
|
||||
}
|
||||
|
||||
private delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
internal bool IsInstalled => _handle != IntPtr.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Ставит хук. Возвращает <c>false</c>, если система отказала.
|
||||
/// </summary>
|
||||
internal bool Install()
|
||||
{
|
||||
if (_handle != IntPtr.Zero)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_handle = SetWindowsHookEx(WhKeyboardLowLevel, _callback, GetModuleHandle(null), 0);
|
||||
return _handle != IntPtr.Zero;
|
||||
}
|
||||
|
||||
internal void Uninstall()
|
||||
{
|
||||
if (_handle == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UnhookWindowsHookEx(_handle);
|
||||
_handle = IntPtr.Zero;
|
||||
}
|
||||
|
||||
public void Dispose() => Uninstall();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr CallNextHookEx(IntPtr hhk, int code, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr GetModuleHandle(string? lpModuleName);
|
||||
|
||||
private IntPtr OnHookEvent(int code, IntPtr wParam, IntPtr lParam)
|
||||
{
|
||||
if (code != HcAction)
|
||||
{
|
||||
return CallNextHookEx(_handle, code, wParam, lParam);
|
||||
}
|
||||
|
||||
var message = (int)wParam;
|
||||
bool isKeyDown = message is WmKeyDown or WmSysKeyDown;
|
||||
bool isKeyUp = message is WmKeyUp or WmSysKeyUp;
|
||||
|
||||
var data = Marshal.PtrToStructure<KeyboardHookData>(lParam);
|
||||
|
||||
// Синтетический ввод присылают экранные клавиатуры, программы
|
||||
// автозамены и средства автоматизации: подменять их работу не наше дело
|
||||
bool injected = (data.flags & LowLevelKeyHookFlagInjected) != 0;
|
||||
|
||||
if ((isKeyDown || isKeyUp) && !injected && _filter((int)data.vkCode, isKeyDown))
|
||||
{
|
||||
// Ненулевой результат вместо CallNextHookEx обрывает цепочку: событие
|
||||
// не дойдёт ни до приложения, ни до обработчика регистра в Windows
|
||||
return 1;
|
||||
}
|
||||
|
||||
return CallNextHookEx(_handle, code, wParam, lParam);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct KeyboardHookData
|
||||
{
|
||||
public uint vkCode;
|
||||
public uint scanCode;
|
||||
public uint flags;
|
||||
public uint time;
|
||||
public IntPtr dwExtraInfo;
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,20 @@ public sealed partial class AppSettings : ObservableObject
|
||||
[ObservableProperty]
|
||||
private double _durationMilliseconds = 500;
|
||||
|
||||
/// <summary>
|
||||
/// Перехватывать Caps Lock и переключать по ней раскладку вместо смены регистра.
|
||||
/// По умолчанию выключено: приложение не должно менять поведение системы,
|
||||
/// пока его об этом не попросили.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private bool _useCapsLockHotkey;
|
||||
|
||||
/// <summary>
|
||||
/// С какого времени удержания Caps Lock переключение отменяется, в миллисекундах.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private double _capsLockHoldMilliseconds = 300;
|
||||
|
||||
[ObservableProperty]
|
||||
private Color _backgroundColor = Color.FromRgb(0x20, 0x20, 0x20);
|
||||
|
||||
@@ -64,4 +78,7 @@ public sealed partial class AppSettings : ObservableObject
|
||||
|
||||
[JsonIgnore]
|
||||
public TimeSpan Duration => TimeSpan.FromMilliseconds(DurationMilliseconds);
|
||||
|
||||
[JsonIgnore]
|
||||
public TimeSpan CapsLockHoldDelay => TimeSpan.FromMilliseconds(CapsLockHoldMilliseconds);
|
||||
}
|
||||
|
||||
@@ -172,4 +172,16 @@
|
||||
<data name="MillisecondsSuffix" xml:space="preserve">
|
||||
<value>ms</value>
|
||||
</data>
|
||||
<data name="CapsLockLabel" xml:space="preserve">
|
||||
<value>Caps Lock</value>
|
||||
</data>
|
||||
<data name="CapsLockHotkeyCheck" xml:space="preserve">
|
||||
<value>Switch the layout instead of changing case</value>
|
||||
</data>
|
||||
<data name="CapsLockHoldLabel" xml:space="preserve">
|
||||
<value>Hold threshold</value>
|
||||
</data>
|
||||
<data name="CapsLockHoldHint" xml:space="preserve">
|
||||
<value>Holding the key longer shows the tooltip without switching the layout.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -172,4 +172,16 @@
|
||||
<data name="MillisecondsSuffix" xml:space="preserve">
|
||||
<value>мс</value>
|
||||
</data>
|
||||
<data name="CapsLockLabel" xml:space="preserve">
|
||||
<value>Caps Lock</value>
|
||||
</data>
|
||||
<data name="CapsLockHotkeyCheck" xml:space="preserve">
|
||||
<value>Переключать раскладку вместо смены регистра</value>
|
||||
</data>
|
||||
<data name="CapsLockHoldLabel" xml:space="preserve">
|
||||
<value>Порог удержания</value>
|
||||
</data>
|
||||
<data name="CapsLockHoldHint" xml:space="preserve">
|
||||
<value>Более долгое нажатие показывает подсказку и не переключает раскладку.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
using System.Windows.Threading;
|
||||
using CursorLang.Interop;
|
||||
using CursorLang.Models;
|
||||
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Держит системный перехват Caps Lock и отличает короткое нажатие от удержания.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Различить их можно только по факту отпускания клавиши, поэтому перехватываются
|
||||
/// оба события — и нажатие, и отпускание. Заодно это единственный способ отменить
|
||||
/// смену регистра: Windows переключает Caps Lock по событию нажатия, и пропустить
|
||||
/// его «на всякий случай» нельзя.
|
||||
/// </remarks>
|
||||
public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable
|
||||
{
|
||||
private const int VirtualKeyCapsLock = 0x14;
|
||||
|
||||
private readonly AppSettings _settings;
|
||||
private readonly LowLevelKeyboardHook _hook;
|
||||
private readonly DispatcherTimer _holdTimer = new();
|
||||
private readonly Dispatcher _dispatcher = Dispatcher.CurrentDispatcher;
|
||||
|
||||
private bool _isPressed;
|
||||
private bool _isHolding;
|
||||
|
||||
public CapsLockHotkeyService(AppSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
_hook = new LowLevelKeyboardHook(OnKeyEvent);
|
||||
_holdTimer.Tick += OnHoldTimerTick;
|
||||
}
|
||||
|
||||
public event EventHandler? Tapped;
|
||||
|
||||
public event EventHandler? HoldStarted;
|
||||
|
||||
public event EventHandler? HoldEnded;
|
||||
|
||||
public bool IsRunning => _hook.IsInstalled;
|
||||
|
||||
public void Start() => _hook.Install();
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_hook.Uninstall();
|
||||
ResetPress();
|
||||
}
|
||||
|
||||
// При закрытии приложения событий уже никто не ждёт, поэтому в отличие
|
||||
// от Stop состояние сбрасывается молча: очередь диспетчера в этот момент
|
||||
// может быть закрыта
|
||||
public void Dispose()
|
||||
{
|
||||
_holdTimer.Tick -= OnHoldTimerTick;
|
||||
_holdTimer.Stop();
|
||||
_isPressed = false;
|
||||
_isHolding = false;
|
||||
_hook.Dispose();
|
||||
}
|
||||
|
||||
// Вызывается системным хуком, то есть внутри разбора очереди сообщений.
|
||||
// Здесь только учёт состояния: показывать окна и рассылать события отсюда
|
||||
// нельзя — обработчик обязан вернуть управление за считанные миллисекунды
|
||||
private bool OnKeyEvent(int virtualKey, bool isKeyDown)
|
||||
{
|
||||
if (virtualKey != VirtualKeyCapsLock)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isKeyDown)
|
||||
{
|
||||
// Пока клавишу держат, Windows повторяет нажатие: отсчёт удержания
|
||||
// ведём от первого события и на повторы не реагируем
|
||||
if (!_isPressed)
|
||||
{
|
||||
_isPressed = true;
|
||||
|
||||
// Порог читаем при каждом нажатии: его меняют в настройках на лету
|
||||
_holdTimer.Interval = _settings.CapsLockHoldDelay;
|
||||
_holdTimer.Start();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_isPressed = false;
|
||||
_holdTimer.Stop();
|
||||
|
||||
if (_isHolding)
|
||||
{
|
||||
_isHolding = false;
|
||||
Notify(HoldEnded);
|
||||
}
|
||||
else
|
||||
{
|
||||
Notify(Tapped);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnHoldTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
_holdTimer.Stop();
|
||||
_isHolding = true;
|
||||
HoldStarted?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
// Событие уходит подписчикам после возврата из хука: они вправе показывать
|
||||
// окна и делать что угодно ещё, не задерживая обработку нажатия
|
||||
private void Notify(EventHandler? handler)
|
||||
{
|
||||
if (handler is not null)
|
||||
{
|
||||
_dispatcher.BeginInvoke(() => handler(this, EventArgs.Empty));
|
||||
}
|
||||
}
|
||||
|
||||
// Перехват могли снять с зажатой клавишей — например, сняв галочку
|
||||
// в настройках. Подсказку в этом случае нужно убрать
|
||||
private void ResetPress()
|
||||
{
|
||||
_isPressed = false;
|
||||
_holdTimer.Stop();
|
||||
|
||||
if (_isHolding)
|
||||
{
|
||||
_isHolding = false;
|
||||
Notify(HoldEnded);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System.ComponentModel;
|
||||
using CursorLang.Models;
|
||||
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Превращает нажатия Caps Lock в действия: короткое — переключает раскладку,
|
||||
/// длинное — только показывает подсказку. Он же включает и выключает перехват
|
||||
/// вслед за галочкой в настройках.
|
||||
/// </summary>
|
||||
public sealed class CapsLockSwitchCoordinator : IDisposable
|
||||
{
|
||||
private readonly ICapsLockHotkeyService _hotkeyService;
|
||||
private readonly IKeyboardLayoutService _layoutService;
|
||||
private readonly ILayoutPopupService _popupService;
|
||||
private readonly AppSettings _settings;
|
||||
|
||||
public CapsLockSwitchCoordinator(
|
||||
ICapsLockHotkeyService hotkeyService,
|
||||
IKeyboardLayoutService layoutService,
|
||||
ILayoutPopupService popupService,
|
||||
AppSettings settings)
|
||||
{
|
||||
_hotkeyService = hotkeyService;
|
||||
_layoutService = layoutService;
|
||||
_popupService = popupService;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_hotkeyService.Tapped += OnTapped;
|
||||
_hotkeyService.HoldStarted += OnHoldStarted;
|
||||
_hotkeyService.HoldEnded += OnHoldEnded;
|
||||
_settings.PropertyChanged += OnSettingsChanged;
|
||||
|
||||
ApplySetting();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_settings.PropertyChanged -= OnSettingsChanged;
|
||||
_hotkeyService.Tapped -= OnTapped;
|
||||
_hotkeyService.HoldStarted -= OnHoldStarted;
|
||||
_hotkeyService.HoldEnded -= OnHoldEnded;
|
||||
_hotkeyService.Stop();
|
||||
}
|
||||
|
||||
private void OnSettingsChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(AppSettings.UseCapsLockHotkey))
|
||||
{
|
||||
ApplySetting();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySetting()
|
||||
{
|
||||
if (_settings.UseCapsLockHotkey)
|
||||
{
|
||||
_hotkeyService.Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
_hotkeyService.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTapped(object? sender, EventArgs e) => _layoutService.SwitchToNext();
|
||||
|
||||
// Раскладку не меняем, но и молчать нельзя: без подсказки долгое нажатие
|
||||
// выглядит как то, что клавиша просто не сработала
|
||||
private void OnHoldStarted(object? sender, EventArgs e) =>
|
||||
_popupService.ShowUntilHidden(_layoutService.Current);
|
||||
|
||||
private void OnHoldEnded(object? sender, EventArgs e) => _popupService.Hide();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Перехватывает Caps Lock на уровне системы и делит нажатия на короткие
|
||||
/// и длинные. Что делать с ними — решают подписчики.
|
||||
/// </summary>
|
||||
public interface ICapsLockHotkeyService
|
||||
{
|
||||
/// <summary>Короткое нажатие: клавишу отпустили до порога удержания.</summary>
|
||||
event EventHandler? Tapped;
|
||||
|
||||
/// <summary>Порог удержания пройден, клавишу всё ещё держат.</summary>
|
||||
event EventHandler? HoldStarted;
|
||||
|
||||
/// <summary>Удержание закончилось: клавишу отпустили.</summary>
|
||||
event EventHandler? HoldEnded;
|
||||
|
||||
/// <summary>Стоит ли перехват прямо сейчас.</summary>
|
||||
bool IsRunning { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Начинает перехват. Вызывать нужно из потока пользовательского интерфейса:
|
||||
/// системный хук клавиатуры работает только на потоке с циклом сообщений.
|
||||
/// </summary>
|
||||
void Start();
|
||||
|
||||
/// <summary>Снимает перехват, возвращая клавише её обычное поведение.</summary>
|
||||
void Stop();
|
||||
}
|
||||
@@ -3,7 +3,8 @@ using CursorLang.Models;
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Следит за раскладкой активного окна — в том числе в чужих приложениях.
|
||||
/// Следит за раскладкой активного окна — в том числе в чужих приложениях —
|
||||
/// и умеет её переключать.
|
||||
/// </summary>
|
||||
public interface IKeyboardLayoutService
|
||||
{
|
||||
@@ -15,4 +16,6 @@ public interface IKeyboardLayoutService
|
||||
void Start();
|
||||
|
||||
void Stop();
|
||||
|
||||
void SwitchToNext();
|
||||
}
|
||||
|
||||
@@ -7,5 +7,14 @@ namespace CursorLang.Services;
|
||||
/// </summary>
|
||||
public interface ILayoutPopupService
|
||||
{
|
||||
/// <summary>Показывает подсказку и убирает её через заданное в настройках время.</summary>
|
||||
void Show(KeyboardLayout layout);
|
||||
|
||||
/// <summary>
|
||||
/// Показывает подсказку до явного вызова <see cref="Hide"/>: нужна там, где
|
||||
/// время показа задаёт не таймер, а действие пользователя.
|
||||
/// </summary>
|
||||
void ShowUntilHidden(KeyboardLayout layout);
|
||||
|
||||
void Hide();
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ public sealed class KeyboardLayoutService : IKeyboardLayoutService, IDisposable
|
||||
|
||||
public void Stop() => _pollTimer.Stop();
|
||||
|
||||
public void SwitchToNext() => KeyboardLayoutNative.RequestNextLayout();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_pollTimer.Stop();
|
||||
|
||||
@@ -27,16 +27,28 @@ public sealed class LayoutPopupService : ILayoutPopupService, IDisposable
|
||||
|
||||
public void Show(KeyboardLayout layout)
|
||||
{
|
||||
_viewModel.ShortName = layout.ShortName;
|
||||
_window.ShowPopup();
|
||||
ShowUntilHidden(layout);
|
||||
|
||||
// Длительность читаем при каждом показе: её меняют в настройках на лету.
|
||||
// Перезапуск таймера заодно продлевает показ при быстрых переключениях
|
||||
_hideTimer.Stop();
|
||||
_hideTimer.Interval = _settings.Duration;
|
||||
_hideTimer.Start();
|
||||
}
|
||||
|
||||
public void ShowUntilHidden(KeyboardLayout layout)
|
||||
{
|
||||
_hideTimer.Stop();
|
||||
|
||||
_viewModel.ShortName = layout.ShortName;
|
||||
_window.ShowPopup();
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
_hideTimer.Stop();
|
||||
_window.Hide();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_hideTimer.Stop();
|
||||
|
||||
@@ -152,6 +152,56 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ======================= CheckBox ======================= -->
|
||||
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Theme.Foreground}" />
|
||||
<Setter Property="MinHeight" Value="24" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="CheckBox">
|
||||
<!-- Прозрачный фон делает щелчок по подписи попаданием в галочку -->
|
||||
<StackPanel Orientation="Horizontal" Background="Transparent">
|
||||
<Border x:Name="Box"
|
||||
Width="18" Height="18"
|
||||
CornerRadius="4"
|
||||
VerticalAlignment="Center"
|
||||
Background="{DynamicResource Theme.ControlBackground}"
|
||||
BorderBrush="{DynamicResource Theme.ControlBorder}"
|
||||
BorderThickness="1"
|
||||
SnapsToDevicePixels="True">
|
||||
<Path x:Name="Check"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Data="M0,4 L3.5,7.5 L9.5,0.5"
|
||||
Stroke="White"
|
||||
StrokeThickness="1.8"
|
||||
StrokeStartLineCap="Round" StrokeEndLineCap="Round"
|
||||
Visibility="Collapsed" />
|
||||
</Border>
|
||||
<ContentPresenter Margin="8,0,0,0" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Box" Property="Background"
|
||||
Value="{DynamicResource Theme.ControlHoverBackground}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Box" Property="Background"
|
||||
Value="{DynamicResource Theme.Accent}" />
|
||||
<Setter TargetName="Box" Property="BorderBrush"
|
||||
Value="{DynamicResource Theme.Accent}" />
|
||||
<Setter TargetName="Check" Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ======================= Slider ======================= -->
|
||||
|
||||
<Style x:Key="SliderThumbStyle" TargetType="Thumb">
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<local:EnumToVisibilityConverter x:Key="EnumToVisibility" />
|
||||
<local:ColorToBrushConverter x:Key="ColorToBrush" />
|
||||
<local:ColorToHexConverter x:Key="ColorToHex" />
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVisibility" />
|
||||
|
||||
<DataTemplate x:Key="EnumOptionTemplate">
|
||||
<TextBlock Text="{Binding Display}" />
|
||||
@@ -286,6 +287,13 @@
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Style="{StaticResource FieldLabel}"
|
||||
Text="{Binding Localization[DurationLabel]}" />
|
||||
<Slider Grid.Column="1" Minimum="200" Maximum="5000" TickFrequency="100"
|
||||
@@ -297,6 +305,41 @@
|
||||
Foreground="{DynamicResource Theme.SecondaryForeground}"
|
||||
Text="{Binding Localization[MillisecondsSuffix]}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Row="1" Margin="0,12,12,0"
|
||||
Style="{StaticResource FieldLabel}"
|
||||
Text="{Binding Localization[CapsLockLabel]}" />
|
||||
<CheckBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||
IsChecked="{Binding Settings.UseCapsLockHotkey}"
|
||||
Content="{Binding Localization[CapsLockHotkeyCheck]}" />
|
||||
|
||||
<TextBlock Grid.Row="2" Margin="0,12,12,0"
|
||||
Style="{StaticResource FieldLabel}"
|
||||
Text="{Binding Localization[CapsLockHoldLabel]}"
|
||||
Visibility="{Binding Settings.UseCapsLockHotkey,
|
||||
Converter={StaticResource BoolToVisibility}}" />
|
||||
<Slider Grid.Row="2" Grid.Column="1" Margin="0,12,0,0"
|
||||
Minimum="150" Maximum="1500" TickFrequency="50"
|
||||
Value="{Binding Settings.CapsLockHoldMilliseconds}"
|
||||
Visibility="{Binding Settings.UseCapsLockHotkey,
|
||||
Converter={StaticResource BoolToVisibility}}" />
|
||||
<StackPanel Grid.Row="2" Grid.Column="2" Margin="0,12,0,0"
|
||||
Orientation="Horizontal" VerticalAlignment="Center"
|
||||
Visibility="{Binding Settings.UseCapsLockHotkey,
|
||||
Converter={StaticResource BoolToVisibility}}">
|
||||
<TextBlock Style="{StaticResource FieldValue}"
|
||||
Text="{Binding Settings.CapsLockHoldMilliseconds, StringFormat={}{0:F0}}" />
|
||||
<TextBlock Margin="4,0,0,0"
|
||||
Foreground="{DynamicResource Theme.SecondaryForeground}"
|
||||
Text="{Binding Localization[MillisecondsSuffix]}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
Margin="0,6,0,0" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource Theme.SecondaryForeground}"
|
||||
Text="{Binding Localization[CapsLockHoldHint]}"
|
||||
Visibility="{Binding Settings.UseCapsLockHotkey,
|
||||
Converter={StaticResource BoolToVisibility}}" />
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user