diff --git a/CHANGELOG.md b/CHANGELOG.md
index 327d005..bfed864 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [1.6.0] - 2026-07-16
+
+### Added
+
+- A configurable **Ctrl+Shift+K** hotkey that toggles the live keyboard-shortcut display.
+- An on-screen badge that announces the current mode (color-coded) when you use the
+ toggle hotkey.
+- **Screen zoom** (default **Ctrl+Shift+Z**): a ZoomIt-style full-screen zoom that
+ freezes the monitor under the cursor and lets you scroll to zoom (up to 6×) and move
+ the mouse to pan, keeping the point under the cursor fixed. Scrolling all the way
+ back out, Esc, right-click, or the hotkey again exits. The hotkey is configurable.
+
+### Changed
+
+- The toggle hotkey now **cycles Off → ClickLight → Laser Pointer** instead of a plain
+ on/off toggle.
+
## [1.5.0] - 2026-07-16
### Added
@@ -134,7 +151,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Initial release: fading click pulses drawn at the cursor, a system-tray
presence, a transparent click-through overlay, and a system-wide mouse hook.
-[Unreleased]: https://github.com/dvdstelt/ClickLightWin/compare/v1.5.0...HEAD
+[Unreleased]: https://github.com/dvdstelt/ClickLightWin/compare/v1.6.0...HEAD
+[1.6.0]: https://github.com/dvdstelt/ClickLightWin/compare/v1.5.0...v1.6.0
[1.5.0]: https://github.com/dvdstelt/ClickLightWin/compare/v1.4.0...v1.5.0
[1.4.0]: https://github.com/dvdstelt/ClickLightWin/compare/v1.3.0...v1.4.0
[1.3.0]: https://github.com/dvdstelt/ClickLightWin/compare/v1.2.0...v1.3.0
diff --git a/README.md b/README.md
index af0f0b0..93953be 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ On each click, a short animated pulse is drawn at the cursor and fades out. The
- Click highlights across macOS apps
- Separate visuals for press, release, right-click, and drag
- Optional laser pointer mode with drawing mode
+- Screen zoom: freeze and magnify the display ZoomIt-style (scroll to zoom, move to pan)
- Optional keyboard shortcuts
- Local daily click activity chart with a resettable seven-day history
- Dedicated settings window with presets, profiles, and a sidebar preview pad with Randomize
@@ -38,7 +39,9 @@ ClickLight includes one default global shortcut for quick toggles during demos.
| Shortcut | Action |
| --- | --- |
-| `Control + Shift + L` | Toggle ClickLight on/off |
+| `Control + Shift + L` | Cycle mode: off -> ClickLight -> laser pointer |
+| `Control + Shift + Z` | Screen zoom (scroll to zoom, move to pan; scroll out, Esc, or right-click to exit) |
+| `Control + Shift + K` | Toggle the keyboard-shortcut display |
| `Control + Shift + D` | Toggle drawing mode, cleared by exiting drawing mode. |
| `Control + Shift + left-click` & drag | Draw an arrow |
| `Control + Shift + right-click` & drag | Draw a box |
diff --git a/src/ClickLightWin/AppController.cs b/src/ClickLightWin/AppController.cs
index 782b47c..063b66b 100644
--- a/src/ClickLightWin/AppController.cs
+++ b/src/ClickLightWin/AppController.cs
@@ -23,6 +23,7 @@ public sealed class AppController : IDisposable
private readonly LaunchAtLoginController _launchAtLogin = new();
private readonly ProfileStore _profileStore = new();
private readonly ActivityStore _activity = new();
+ private readonly ZoomController _zoom = new();
private readonly UpdateService _updates = new();
private Settings _settings = new();
private TrayIcon? _tray;
@@ -63,9 +64,11 @@ 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.ZoomPressed += () => _zoom.Toggle();
_hotKeys.Start();
ConfigureHotkeys();
@@ -147,7 +150,8 @@ private void UpdateKeyboardHook()
private void ConfigureHotkeys()
{
- _hotKeys.Configure(_settings.ToggleHotKey, _settings.ClearHotKey, _settings.DrawModeHotKey);
+ _hotKeys.Configure(_settings.ToggleHotKey, _settings.ClearHotKey, _settings.DrawModeHotKey,
+ _settings.ShortcutsHotKey, _settings.ZoomHotKey);
WarnAboutUnavailableHotkeys();
}
@@ -159,6 +163,8 @@ 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 (!_hotKeys.ZoomRegistered) taken.Add($"{_settings.ZoomHotKey.Display} (zoom)");
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 +245,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()
@@ -287,6 +331,7 @@ public void Dispose()
_keyboardHook.Dispose();
_hotKeys.Dispose();
_overlays?.Dispose();
+ _zoom.Dispose();
_tray?.Dispose();
}
}
diff --git a/src/ClickLightWin/HotKeyBinding.cs b/src/ClickLightWin/HotKeyBinding.cs
index 8df3ebb..13653ff 100644
--- a/src/ClickLightWin/HotKeyBinding.cs
+++ b/src/ClickLightWin/HotKeyBinding.cs
@@ -52,6 +52,8 @@ 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);
+ public static readonly HotKeyBinding DefaultZoom = new(ModifierKeys.Control | ModifierKeys.Shift, Key.Z);
private static string KeyName(Key key) => key switch
{
diff --git a/src/ClickLightWin/Interop/HotKeyManager.cs b/src/ClickLightWin/Interop/HotKeyManager.cs
index 6a1d821..8237d69 100644
--- a/src/ClickLightWin/Interop/HotKeyManager.cs
+++ b/src/ClickLightWin/Interop/HotKeyManager.cs
@@ -14,18 +14,24 @@ 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 const int ZoomHotKeyId = 5;
private HwndSource? _source;
- private HotKeyBinding? _toggle, _clear, _drawMode;
+ private HotKeyBinding? _toggle, _clear, _drawMode, _shortcuts, _zoom;
public event Action? TogglePressed;
public event Action? ClearPressed;
public event Action? DrawModePressed;
+ public event Action? ShortcutsPressed;
+ public event Action? ZoomPressed;
/// 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; }
+ public bool ZoomRegistered { get; private set; }
/// Create the message-only window that receives WM_HOTKEY.
public void Start()
@@ -42,11 +48,14 @@ 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, HotKeyBinding zoom)
{
_toggle = toggle;
_clear = clear;
_drawMode = drawMode;
+ _shortcuts = shortcuts;
+ _zoom = zoom;
Reregister();
}
@@ -60,6 +69,8 @@ private void Reregister()
ToggleRegistered = TryRegister(ToggleHotKeyId, _toggle);
ClearRegistered = TryRegister(ClearHotKeyId, _clear);
DrawModeRegistered = TryRegister(DrawModeHotKeyId, _drawMode);
+ ShortcutsRegistered = TryRegister(ShortcutsHotKeyId, _shortcuts);
+ ZoomRegistered = TryRegister(ZoomHotKeyId, _zoom);
}
private bool TryRegister(int id, HotKeyBinding? binding) =>
@@ -72,7 +83,9 @@ 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);
+ NativeMethods.UnregisterHotKey(_source.Handle, ZoomHotKeyId);
+ ToggleRegistered = ClearRegistered = DrawModeRegistered = ShortcutsRegistered = ZoomRegistered = false;
}
private nint WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled)
@@ -83,6 +96,8 @@ 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;
+ case ZoomHotKeyId: ZoomPressed?.Invoke(); handled = true; break;
}
return 0;
}
diff --git a/src/ClickLightWin/Interop/NativeMethods.cs b/src/ClickLightWin/Interop/NativeMethods.cs
index 4b57a3a..fc684ae 100644
--- a/src/ClickLightWin/Interop/NativeMethods.cs
+++ b/src/ClickLightWin/Interop/NativeMethods.cs
@@ -89,6 +89,11 @@ public static partial bool SetWindowPos(
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool DestroyIcon(nint hIcon);
+ // Free a GDI object (e.g. the HBITMAP from a screen capture).
+ [LibraryImport("gdi32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static partial bool DeleteObject(nint hObject);
+
// ---- Foreground activation -----------------------------------------------
// A tray context menu shown by hand never gets foreground activation, so a
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/Overlay/ZoomController.cs b/src/ClickLightWin/Overlay/ZoomController.cs
new file mode 100644
index 0000000..754f83d
--- /dev/null
+++ b/src/ClickLightWin/Overlay/ZoomController.cs
@@ -0,0 +1,68 @@
+using System.Windows;
+using System.Windows.Interop;
+using System.Windows.Media.Imaging;
+using ClickLightWin.Interop;
+using DrawingPoint = System.Drawing.Point;
+using Screen = System.Windows.Forms.Screen;
+
+namespace ClickLightWin.Overlay;
+
+///
+/// Toggles the ZoomIt-style full-screen zoom: on activate it snapshots the monitor under
+/// the cursor and shows it in a that scales and pans around the
+/// cursor. Esc, right-click, or the hotkey again closes it.
+///
+public sealed class ZoomController : IDisposable
+{
+ private ZoomWindow? _window;
+
+ public bool Active => _window is not null;
+
+ public void Toggle()
+ {
+ if (Active) Close();
+ else Open();
+ }
+
+ private void Open()
+ {
+ if (!NativeMethods.GetCursorPos(out var pt)) return;
+ var screen = Screen.FromPoint(new DrawingPoint(pt.X, pt.Y));
+ var window = new ZoomWindow(screen, CaptureScreen(screen)); // capture before showing
+ window.ExitRequested += Close;
+ _window = window;
+ window.Show();
+ window.Activate();
+ }
+
+ private void Close()
+ {
+ if (_window is null) return;
+ _window.ExitRequested -= Close;
+ _window.Close();
+ _window = null;
+ }
+
+ private static BitmapSource CaptureScreen(Screen screen)
+ {
+ var b = screen.Bounds;
+ using var bmp = new System.Drawing.Bitmap(b.Width, b.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
+ using (var g = System.Drawing.Graphics.FromImage(bmp))
+ g.CopyFromScreen(b.X, b.Y, 0, 0, new System.Drawing.Size(b.Width, b.Height));
+
+ var hbitmap = bmp.GetHbitmap();
+ try
+ {
+ var source = Imaging.CreateBitmapSourceFromHBitmap(
+ hbitmap, 0, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
+ source.Freeze();
+ return source;
+ }
+ finally
+ {
+ NativeMethods.DeleteObject(hbitmap);
+ }
+ }
+
+ public void Dispose() => Close();
+}
diff --git a/src/ClickLightWin/Overlay/ZoomWindow.xaml b/src/ClickLightWin/Overlay/ZoomWindow.xaml
new file mode 100644
index 0000000..b6fe898
--- /dev/null
+++ b/src/ClickLightWin/Overlay/ZoomWindow.xaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ClickLightWin/Overlay/ZoomWindow.xaml.cs b/src/ClickLightWin/Overlay/ZoomWindow.xaml.cs
new file mode 100644
index 0000000..b1372f6
--- /dev/null
+++ b/src/ClickLightWin/Overlay/ZoomWindow.xaml.cs
@@ -0,0 +1,89 @@
+using System.Windows;
+using System.Windows.Input;
+using System.Windows.Interop;
+using System.Windows.Media.Imaging;
+using ClickLightWin.Interop;
+using KeyEventArgs = System.Windows.Input.KeyEventArgs;
+using MouseEventArgs = System.Windows.Input.MouseEventArgs;
+using Point = System.Windows.Point;
+using Screen = System.Windows.Forms.Screen;
+
+namespace ClickLightWin.Overlay;
+
+///
+/// A ZoomIt-style full-screen zoom: a frozen snapshot of one monitor shown scaled and
+/// panned around the cursor. Because it is a static image with a render transform, the
+/// zoom/pan is GPU-cheap and stays smooth. Scroll changes zoom; the point under the
+/// cursor stays fixed. Esc or right-click exits.
+///
+public partial class ZoomWindow : Window
+{
+ private const double ZoomStep = 0.25;
+ private const double MinZoom = 1.0;
+ private const double MaxZoom = 6.0;
+
+ private readonly Screen _screen;
+ private double _zoom = 2.0;
+
+ public event Action? ExitRequested;
+
+ public ZoomWindow(Screen screen, BitmapSource capture)
+ {
+ InitializeComponent();
+ _screen = screen;
+ Frozen.Source = capture;
+ Loaded += (_, _) =>
+ {
+ Activate();
+ Focus();
+ UpdateTransform(Mouse.GetPosition(this));
+ };
+ }
+
+ protected override void OnMouseMove(MouseEventArgs e) => UpdateTransform(e.GetPosition(this));
+
+ protected override void OnMouseWheel(MouseWheelEventArgs e)
+ {
+ e.Handled = true;
+ var next = _zoom + (e.Delta > 0 ? ZoomStep : -ZoomStep);
+ if (next <= MinZoom)
+ {
+ ExitRequested?.Invoke(); // zoomed all the way out = leave zoom mode
+ return;
+ }
+ _zoom = Math.Min(next, MaxZoom);
+ UpdateTransform(e.GetPosition(this));
+ }
+
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ if (e.Key == Key.Escape) ExitRequested?.Invoke();
+ }
+
+ protected override void OnMouseRightButtonDown(MouseButtonEventArgs e) => ExitRequested?.Invoke();
+
+ private void UpdateTransform(Point cursor)
+ {
+ Scale.ScaleX = Scale.ScaleY = _zoom;
+ var w = ActualWidth;
+ var h = ActualHeight;
+ // Keep the point under the cursor fixed and magnify around it, clamped so the
+ // image always covers the window (no black edges).
+ Trans.X = Math.Clamp(cursor.X * (1 - _zoom), w * (1 - _zoom), 0);
+ Trans.Y = Math.Clamp(cursor.Y * (1 - _zoom), h * (1 - _zoom), 0);
+ }
+
+ protected override void OnSourceInitialized(EventArgs e)
+ {
+ base.OnSourceInitialized(e);
+ var hwnd = new WindowInteropHelper(this).Handle;
+
+ // Hide from alt-tab but stay activatable so it can take keyboard focus (Esc).
+ var ex = NativeMethods.GetWindowLongPtr(hwnd, NativeMethods.GWL_EXSTYLE);
+ NativeMethods.SetWindowLongPtr(hwnd, NativeMethods.GWL_EXSTYLE, (nint)((long)ex | NativeMethods.WS_EX_TOOLWINDOW));
+
+ var b = _screen.Bounds;
+ NativeMethods.SetWindowPos(hwnd, NativeMethods.HWND_TOPMOST, b.X, b.Y, b.Width, b.Height,
+ NativeMethods.SWP_SHOWWINDOW);
+ }
+}
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..5fcce14 100644
--- a/src/ClickLightWin/Settings.cs
+++ b/src/ClickLightWin/Settings.cs
@@ -37,6 +37,8 @@ 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 HotKeyBinding _zoomHotKey = HotKeyBinding.DefaultZoom;
private string _currentProfileName = ProfileStore.DefaultProfileName;
private ShortcutPosition _shortcutPosition = ShortcutPosition.BottomCenter;
private ShortcutSize _shortcutSize = ShortcutSize.Medium;
@@ -66,6 +68,8 @@ 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); }
+ public HotKeyBinding ZoomHotKey { get => _zoomHotKey; set => Set(ref _zoomHotKey, value); }
/// Name of the profile currently selected in the settings window.
public string CurrentProfileName { get => _currentProfileName; set => Set(ref _currentProfileName, value); }
@@ -163,6 +167,8 @@ public void CopyFrom(Settings other)
ToggleHotKey = other.ToggleHotKey;
ClearHotKey = other.ClearHotKey;
DrawModeHotKey = other.DrawModeHotKey;
+ ShortcutsHotKey = other.ShortcutsHotKey;
+ ZoomHotKey = other.ZoomHotKey;
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..177c3ee 100644
--- a/src/ClickLightWin/Views/SettingsWindow.xaml
+++ b/src/ClickLightWin/Views/SettingsWindow.xaml
@@ -652,16 +652,22 @@
-
+
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/src/ClickLightWin/Views/SettingsWindow.xaml.cs b/src/ClickLightWin/Views/SettingsWindow.xaml.cs
index bc7a7de..1723f2c 100644
--- a/src/ClickLightWin/Views/SettingsWindow.xaml.cs
+++ b/src/ClickLightWin/Views/SettingsWindow.xaml.cs
@@ -142,6 +142,8 @@ private void OnResetShortcut(object sender, RoutedEventArgs e)
case "toggle": settings.ToggleHotKey = HotKeyBinding.DefaultToggle; break;
case "clear": settings.ClearHotKey = HotKeyBinding.DefaultClear; break;
case "draw": settings.DrawModeHotKey = HotKeyBinding.DefaultDrawMode; break;
+ case "shortcuts": settings.ShortcutsHotKey = HotKeyBinding.DefaultShortcuts; break;
+ case "zoom": settings.ZoomHotKey = HotKeyBinding.DefaultZoom; break;
}
}