From 7dceca340931bd44359ee35eb7372b7c4c596288 Mon Sep 17 00:00:00 2001 From: Dennis van der Stelt Date: Thu, 16 Jul 2026 17:04:21 +0200 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8=20Cycle=20the=20toggle=20hotkey?= =?UTF-8?q?=20through=20modes=20and=20add=20a=20shortcut-display=20hotkey?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The toggle hotkey now cycles Off -> ClickLight -> Laser Pointer -> Off instead of plain on/off, and each switch shows a color-coded toast at the top-center (reusing the shortcut-pill layout, blue/red/gray) so the new mode is clear. Add a fourth configurable global hotkey (Ctrl+Shift+K) that toggles the live keyboard-shortcut display, with its own recorder row and reset in settings and a toast confirming the new state. --- src/ClickLightWin/AppController.cs | 52 ++++++++++++++++--- src/ClickLightWin/HotKeyBinding.cs | 1 + src/ClickLightWin/Interop/HotKeyManager.cs | 13 +++-- src/ClickLightWin/Overlay/OverlayManager.cs | 9 ++++ src/ClickLightWin/Overlay/OverlayWindow.xaml | 3 ++ .../Overlay/OverlayWindow.xaml.cs | 8 +++ .../Rendering/ModeIndicatorRenderer.cs | 32 ++++++++++++ src/ClickLightWin/Settings.cs | 3 ++ src/ClickLightWin/Views/SettingsWindow.xaml | 11 ++-- .../Views/SettingsWindow.xaml.cs | 1 + 10 files changed, 120 insertions(+), 13 deletions(-) create mode 100644 src/ClickLightWin/Rendering/ModeIndicatorRenderer.cs diff --git a/src/ClickLightWin/AppController.cs b/src/ClickLightWin/AppController.cs index 782b47c..b09e1d4 100644 --- a/src/ClickLightWin/AppController.cs +++ b/src/ClickLightWin/AppController.cs @@ -63,9 +63,10 @@ public void Start() _overlays = new OverlayManager(_settings); // Global hotkeys (defaults Ctrl+Shift+L/C/D; user-configurable in settings). - _hotKeys.TogglePressed += ToggleEnabled; + _hotKeys.TogglePressed += CycleMode; _hotKeys.ClearPressed += ClearAnnotations; _hotKeys.DrawModePressed += ToggleDrawMode; + _hotKeys.ShortcutsPressed += ToggleShortcuts; _hotKeys.Start(); ConfigureHotkeys(); @@ -147,7 +148,7 @@ private void UpdateKeyboardHook() private void ConfigureHotkeys() { - _hotKeys.Configure(_settings.ToggleHotKey, _settings.ClearHotKey, _settings.DrawModeHotKey); + _hotKeys.Configure(_settings.ToggleHotKey, _settings.ClearHotKey, _settings.DrawModeHotKey, _settings.ShortcutsHotKey); WarnAboutUnavailableHotkeys(); } @@ -159,6 +160,7 @@ private void WarnAboutUnavailableHotkeys() if (!_hotKeys.ToggleRegistered) taken.Add($"{_settings.ToggleHotKey.Display} (toggle)"); if (!_hotKeys.ClearRegistered) taken.Add($"{_settings.ClearHotKey.Display} (clear annotations)"); if (!_hotKeys.DrawModeRegistered) taken.Add($"{_settings.DrawModeHotKey.Display} (drawing mode)"); + if (!_hotKeys.ShortcutsRegistered) taken.Add($"{_settings.ShortcutsHotKey.Display} (keyboard shortcuts)"); if (taken.Count == 0) return; _tray?.ShowWarning("ClickLight shortcut unavailable", $"{string.Join(" and ", taken)} could not be registered (already in use or invalid)."); @@ -239,12 +241,50 @@ private void RecordActivity(ClickEvent click) } } - // The global hotkey path. The tray menu toggles Enabled on its own; both just - // mutate the shared settings, and the tray refreshes its checkmarks on open. - private void ToggleEnabled() + // The toggle hotkey cycles through three modes: Off -> ClickLight (pulses only) + // -> Laser Pointer (pulses + laser glow) -> Off. The tray still toggles Enabled and + // the laser independently; the tray refreshes its checkmarks on open. + private void CycleMode() { - _settings.Enabled = !_settings.Enabled; + if (!_settings.Enabled) + { + _settings.ShowLaserPointer = false; // Off -> ClickLight + _settings.Enabled = true; + } + else if (!_settings.ShowLaserPointer) + { + _settings.ShowLaserPointer = true; // ClickLight -> Laser Pointer + } + else + { + _settings.Enabled = false; // Laser Pointer -> Off + } + _settingsStore.Save(_settings); + AnnounceMode(); + } + + // Show a transient badge naming the mode we just switched to, color-coded so it + // stands apart from the live keyboard-shortcut pills. + private void AnnounceMode() + { + var (text, color) = !_settings.Enabled + ? ("Off", System.Windows.Media.Color.FromRgb(0x9A, 0x9A, 0xA2)) + : !_settings.ShowLaserPointer + ? ("ClickLight", System.Windows.Media.Color.FromRgb(0x3B, 0x82, 0xF6)) + : ("Laser Pointer", System.Windows.Media.Color.FromRgb(0xFF, 0x29, 0x05)); + _overlays?.ShowMode(text, color); + } + + // Toggle the live keyboard-shortcut display. Since it is otherwise invisible until + // the next shortcut is pressed, announce the new state with a toast. + private void ToggleShortcuts() + { + _settings.ShowShortcuts = !_settings.ShowShortcuts; _settingsStore.Save(_settings); + var (text, color) = _settings.ShowShortcuts + ? ("Keyboard Shortcuts On", System.Windows.Media.Color.FromRgb(0x3B, 0x82, 0xF6)) + : ("Keyboard Shortcuts Off", System.Windows.Media.Color.FromRgb(0x9A, 0x9A, 0xA2)); + _overlays?.ShowMode(text, color); } private void ShowSettings() diff --git a/src/ClickLightWin/HotKeyBinding.cs b/src/ClickLightWin/HotKeyBinding.cs index 8df3ebb..dd025b6 100644 --- a/src/ClickLightWin/HotKeyBinding.cs +++ b/src/ClickLightWin/HotKeyBinding.cs @@ -52,6 +52,7 @@ public string Display public static readonly HotKeyBinding DefaultToggle = new(ModifierKeys.Control | ModifierKeys.Shift, Key.L); public static readonly HotKeyBinding DefaultClear = new(ModifierKeys.Control | ModifierKeys.Shift, Key.C); public static readonly HotKeyBinding DefaultDrawMode = new(ModifierKeys.Control | ModifierKeys.Shift, Key.D); + public static readonly HotKeyBinding DefaultShortcuts = new(ModifierKeys.Control | ModifierKeys.Shift, Key.K); private static string KeyName(Key key) => key switch { diff --git a/src/ClickLightWin/Interop/HotKeyManager.cs b/src/ClickLightWin/Interop/HotKeyManager.cs index 6a1d821..47537da 100644 --- a/src/ClickLightWin/Interop/HotKeyManager.cs +++ b/src/ClickLightWin/Interop/HotKeyManager.cs @@ -14,18 +14,21 @@ public sealed class HotKeyManager : IDisposable private const int ToggleHotKeyId = 1; private const int ClearHotKeyId = 2; private const int DrawModeHotKeyId = 3; + private const int ShortcutsHotKeyId = 4; private HwndSource? _source; - private HotKeyBinding? _toggle, _clear, _drawMode; + private HotKeyBinding? _toggle, _clear, _drawMode, _shortcuts; public event Action? TogglePressed; public event Action? ClearPressed; public event Action? DrawModePressed; + public event Action? ShortcutsPressed; /// False when the binding is invalid or another application already owns it. public bool ToggleRegistered { get; private set; } public bool ClearRegistered { get; private set; } public bool DrawModeRegistered { get; private set; } + public bool ShortcutsRegistered { get; private set; } /// Create the message-only window that receives WM_HOTKEY. public void Start() @@ -42,11 +45,12 @@ public void Start() } /// Apply a new set of bindings and (re)register them. - public void Configure(HotKeyBinding toggle, HotKeyBinding clear, HotKeyBinding drawMode) + public void Configure(HotKeyBinding toggle, HotKeyBinding clear, HotKeyBinding drawMode, HotKeyBinding shortcuts) { _toggle = toggle; _clear = clear; _drawMode = drawMode; + _shortcuts = shortcuts; Reregister(); } @@ -60,6 +64,7 @@ private void Reregister() ToggleRegistered = TryRegister(ToggleHotKeyId, _toggle); ClearRegistered = TryRegister(ClearHotKeyId, _clear); DrawModeRegistered = TryRegister(DrawModeHotKeyId, _drawMode); + ShortcutsRegistered = TryRegister(ShortcutsHotKeyId, _shortcuts); } private bool TryRegister(int id, HotKeyBinding? binding) => @@ -72,7 +77,8 @@ private void UnregisterAll() NativeMethods.UnregisterHotKey(_source.Handle, ToggleHotKeyId); NativeMethods.UnregisterHotKey(_source.Handle, ClearHotKeyId); NativeMethods.UnregisterHotKey(_source.Handle, DrawModeHotKeyId); - ToggleRegistered = ClearRegistered = DrawModeRegistered = false; + NativeMethods.UnregisterHotKey(_source.Handle, ShortcutsHotKeyId); + ToggleRegistered = ClearRegistered = DrawModeRegistered = ShortcutsRegistered = false; } private nint WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled) @@ -83,6 +89,7 @@ private nint WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool hand case ToggleHotKeyId: TogglePressed?.Invoke(); handled = true; break; case ClearHotKeyId: ClearPressed?.Invoke(); handled = true; break; case DrawModeHotKeyId: DrawModePressed?.Invoke(); handled = true; break; + case ShortcutsHotKeyId: ShortcutsPressed?.Invoke(); handled = true; break; } return 0; } diff --git a/src/ClickLightWin/Overlay/OverlayManager.cs b/src/ClickLightWin/Overlay/OverlayManager.cs index 6651a45..2da9dbb 100644 --- a/src/ClickLightWin/Overlay/OverlayManager.cs +++ b/src/ClickLightWin/Overlay/OverlayManager.cs @@ -1,4 +1,5 @@ using Microsoft.Win32; // SystemEvents +using Color = System.Windows.Media.Color; using Screen = System.Windows.Forms.Screen; namespace ClickLightWin.Overlay; @@ -106,6 +107,14 @@ public void DispatchShortcut(IReadOnlyList keys) overlay?.ShowShortcut(keys, _settings, pt.X, pt.Y); } + /// Announce a mode switch on the monitor holding the cursor. + public void ShowMode(string text, Color accent) + { + Interop.NativeMethods.GetCursorPos(out var pt); + var overlay = FindByPoint(pt.X, pt.Y) ?? (_overlays.Count > 0 ? _overlays[0] : null); + overlay?.ShowMode(text, accent); + } + private OverlayWindow? Find(ClickEvent click) => FindByPoint(click.ScreenX, click.ScreenY); private OverlayWindow? FindByPoint(int x, int y) diff --git a/src/ClickLightWin/Overlay/OverlayWindow.xaml b/src/ClickLightWin/Overlay/OverlayWindow.xaml index 5fd89fd..91d8245 100644 --- a/src/ClickLightWin/Overlay/OverlayWindow.xaml +++ b/src/ClickLightWin/Overlay/OverlayWindow.xaml @@ -16,6 +16,9 @@ + + diff --git a/src/ClickLightWin/Overlay/OverlayWindow.xaml.cs b/src/ClickLightWin/Overlay/OverlayWindow.xaml.cs index f59d5bb..28a62de 100644 --- a/src/ClickLightWin/Overlay/OverlayWindow.xaml.cs +++ b/src/ClickLightWin/Overlay/OverlayWindow.xaml.cs @@ -24,6 +24,7 @@ public partial class OverlayWindow : Window private LaserRenderer? _laser; private AnnotationRenderer? _annotations; private ShortcutStackRenderer? _shortcuts; + private ModeIndicatorRenderer? _mode; private DrawStrokeRenderer? _drawStrokes; private bool _drawing; private nint _hwnd; @@ -152,6 +153,13 @@ public void ShowShortcut(IReadOnlyList keys, Settings settings, int scre _shortcuts.Show(keys, settings); } + /// Show a transient, color-coded mode-switch badge at the top-center. + public void ShowMode(string text, Color accent) + { + _mode ??= new ModeIndicatorRenderer(ModeHost); + _mode.Show(text, accent); + } + private void ShowShortcutNearPointer(IReadOnlyList keys, Settings settings, int screenX, int screenY) { var pill = ShortcutStackRenderer.BuildPill(keys, settings.ShortcutFontSize); diff --git a/src/ClickLightWin/Rendering/ModeIndicatorRenderer.cs b/src/ClickLightWin/Rendering/ModeIndicatorRenderer.cs new file mode 100644 index 0000000..cf36632 --- /dev/null +++ b/src/ClickLightWin/Rendering/ModeIndicatorRenderer.cs @@ -0,0 +1,32 @@ +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Animation; +using Color = System.Windows.Media.Color; +using Panel = System.Windows.Controls.Panel; + +namespace ClickLightWin.Rendering; + +/// +/// A transient mode-switch toast (ClickLight / Laser Pointer / Off) shown when the +/// toggle hotkey cycles. It reuses the live shortcut display's pill layout, but sits +/// at the top-center with a color-accented border so it still reads as its own thing. +/// +public sealed class ModeIndicatorRenderer(Panel host) +{ + public void Show(string text, Color accent) + { + host.Children.Clear(); // only the latest switch is shown + + var pill = ShortcutStackRenderer.BuildPill([text], 16); + pill.BorderBrush = new SolidColorBrush(accent); + pill.BorderThickness = new Thickness(2); + host.Children.Add(pill); + + var fade = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(300)) + { + BeginTime = TimeSpan.FromMilliseconds(1100) + }; + fade.Completed += (_, _) => host.Children.Remove(pill); + pill.BeginAnimation(UIElement.OpacityProperty, fade); + } +} diff --git a/src/ClickLightWin/Settings.cs b/src/ClickLightWin/Settings.cs index 2073a79..45e73d1 100644 --- a/src/ClickLightWin/Settings.cs +++ b/src/ClickLightWin/Settings.cs @@ -37,6 +37,7 @@ public sealed class Settings : INotifyPropertyChanged private HotKeyBinding _toggleHotKey = HotKeyBinding.DefaultToggle; private HotKeyBinding _clearHotKey = HotKeyBinding.DefaultClear; private HotKeyBinding _drawModeHotKey = HotKeyBinding.DefaultDrawMode; + private HotKeyBinding _shortcutsHotKey = HotKeyBinding.DefaultShortcuts; private string _currentProfileName = ProfileStore.DefaultProfileName; private ShortcutPosition _shortcutPosition = ShortcutPosition.BottomCenter; private ShortcutSize _shortcutSize = ShortcutSize.Medium; @@ -66,6 +67,7 @@ public sealed class Settings : INotifyPropertyChanged public HotKeyBinding ToggleHotKey { get => _toggleHotKey; set => Set(ref _toggleHotKey, value); } public HotKeyBinding ClearHotKey { get => _clearHotKey; set => Set(ref _clearHotKey, value); } public HotKeyBinding DrawModeHotKey { get => _drawModeHotKey; set => Set(ref _drawModeHotKey, value); } + public HotKeyBinding ShortcutsHotKey { get => _shortcutsHotKey; set => Set(ref _shortcutsHotKey, value); } /// Name of the profile currently selected in the settings window. public string CurrentProfileName { get => _currentProfileName; set => Set(ref _currentProfileName, value); } @@ -163,6 +165,7 @@ public void CopyFrom(Settings other) ToggleHotKey = other.ToggleHotKey; ClearHotKey = other.ClearHotKey; DrawModeHotKey = other.DrawModeHotKey; + ShortcutsHotKey = other.ShortcutsHotKey; CurrentProfileName = other.CurrentProfileName; // Deep-copy the menu layout into this instance's own collection (kept, not diff --git a/src/ClickLightWin/Views/SettingsWindow.xaml b/src/ClickLightWin/Views/SettingsWindow.xaml index b4eae2f..ca5a8e4 100644 --- a/src/ClickLightWin/Views/SettingsWindow.xaml +++ b/src/ClickLightWin/Views/SettingsWindow.xaml @@ -652,16 +652,19 @@ - +