Skip to content

Commit 99c8c30

Browse files
Merge pull request #20 from RuntimeRascal/copilot/add-screenshot-hotkey
Add screenshot capture hotkeys (Ctrl+S full screen)
2 parents 9fe9361 + 149af1a commit 99c8c30

19 files changed

+1410
-11
lines changed

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ All notable changes to GhostDraw will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
9+
## v1.0.7
10+
11+
### Added
12+
- **Screenshot Capture** - Capture your drawings as images
13+
- Press `Ctrl+S` to capture full screen with drawings (saved to Pictures\GhostDraw)
14+
- Key suppression prevents Windows from intercepting Ctrl+S during drawing mode
15+
- Optional: Copy to clipboard, open folder, play shutter sound (configurable in settings)
16+
- **Screenshot Settings Panel** - New UI section in Settings window
17+
- Toggle clipboard copy, folder opening, and sound effects
18+
- Configurable save location
19+
- **Key Legend Documentation** - Comprehensive keyboard shortcut reference (`docs/KEY-LEGEND.md`)
20+
21+
### Fixed
22+
- Screenshot hotkey (`Ctrl+S`) now correctly detects Control key by tracking both left (VK_LCONTROL) and right (VK_RCONTROL) control keys instead of generic VK_CONTROL
23+
- Thread safety improvements with volatile field for update nesting level
24+
825
## v1.0.6
926

1027
### Added
@@ -16,10 +33,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1633
- Works with both polylines (pen strokes) and lines (straight line tool)
1734
- Custom eraser cursor with visual feedback
1835
- **Improved Code Quality**
36+
- Added explicit type aliases to resolve WPF/WinForms namespace conflicts
37+
- Fixed `Point`, `Brush`, `Color`, `ColorConverter`, and `Brushes` type ambiguities
1938
- Enhanced tool interface consistency
2039

2140
### Fixed
41+
- Ambiguous reference errors caused by both WPF (`System.Windows`) and WinForms (`System.Drawing`) being enabled
2242
- LineTool's `OnDeactivated` method now properly resets state without calling non-existent method
43+
- Build errors related to namespace conflicts in drawing tool implementations
2344

2445
## v1.0.5
2546

Installer/GhostDraw.Installer.wixproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="WixToolset.Sdk/4.0.5">
22
<PropertyGroup>
3-
<Version Condition="'$(Version)' == ''">1.0.6</Version>
3+
<Version Condition="'$(Version)' == ''">1.0.7</Version>
44
<OutputName>GhostDrawSetup-$(Version)</OutputName>
55
<OutputType>Package</OutputType>
66
<Platform>x64</Platform>

Src/GhostDraw/App.xaml.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ protected override void OnStartup(StartupEventArgs e)
7878
_keyboardHook.LineToolPressed += OnLineToolPressed;
7979
_keyboardHook.EraserToolPressed += OnEraserToolPressed;
8080
_keyboardHook.HelpPressed += OnHelpPressed;
81+
_keyboardHook.ScreenshotFullPressed += OnScreenshotFullPressed;
8182
_keyboardHook.Start();
8283

8384
// Setup system tray icon
@@ -323,6 +324,34 @@ private void OnHelpPressed(object? sender, EventArgs e)
323324
}
324325
}
325326

327+
private void OnScreenshotFullPressed(object? sender, EventArgs e)
328+
{
329+
try
330+
{
331+
_logger?.LogInformation("====== OnScreenshotFullPressed CALLED ======");
332+
_logger?.LogInformation("DrawingManager null: {IsNull}", _drawingManager == null);
333+
_logger?.LogInformation("DrawingManager.IsDrawingMode: {IsDrawingMode}", _drawingManager?.IsDrawingMode);
334+
335+
// Capture full screenshot if drawing mode is active
336+
if (_drawingManager?.IsDrawingMode == true)
337+
{
338+
_logger?.LogInformation("Ctrl+S pressed - capturing full screenshot (calling DrawingManager.CaptureFullScreenshot)");
339+
_drawingManager?.CaptureFullScreenshot();
340+
_logger?.LogInformation("DrawingManager.CaptureFullScreenshot call completed");
341+
}
342+
else
343+
{
344+
_logger?.LogWarning("Screenshot ignored - drawing mode is NOT active");
345+
}
346+
_logger?.LogInformation("====== OnScreenshotFullPressed COMPLETED ======");
347+
}
348+
catch (Exception ex)
349+
{
350+
_logger?.LogError(ex, "Exception in OnScreenshotFullPressed");
351+
_exceptionHandler?.HandleException(ex, "Screenshot full pressed handler");
352+
}
353+
}
354+
326355
protected override void OnExit(ExitEventArgs e)
327356
{
328357
_logger?.LogInformation("Application exiting");

Src/GhostDraw/Core/AppSettings.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,32 @@ public class AppSettings
8181
"#800080" // Purple
8282
};
8383

84+
/// <summary>
85+
/// Directory path where screenshots will be saved
86+
/// </summary>
87+
[JsonPropertyName("screenshotSavePath")]
88+
public string ScreenshotSavePath { get; set; } = System.IO.Path.Combine(
89+
Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
90+
"GhostDraw");
91+
92+
/// <summary>
93+
/// If true, copies screenshot to clipboard after capture
94+
/// </summary>
95+
[JsonPropertyName("copyScreenshotToClipboard")]
96+
public bool CopyScreenshotToClipboard { get; set; } = true;
97+
98+
/// <summary>
99+
/// If true, plays a shutter sound when screenshot is captured
100+
/// </summary>
101+
[JsonPropertyName("playShutterSound")]
102+
public bool PlayShutterSound { get; set; } = false;
103+
104+
/// <summary>
105+
/// If true, opens the folder after screenshot is saved
106+
/// </summary>
107+
[JsonPropertyName("openFolderAfterScreenshot")]
108+
public bool OpenFolderAfterScreenshot { get; set; } = false;
109+
84110
/// <summary>
85111
/// Creates a deep copy of the settings
86112
/// </summary>
@@ -96,7 +122,11 @@ public AppSettings Clone()
96122
HotkeyVirtualKeys = new List<int>(HotkeyVirtualKeys),
97123
LockDrawingMode = LockDrawingMode,
98124
LogLevel = LogLevel,
99-
ColorPalette = new List<string>(ColorPalette)
125+
ColorPalette = new List<string>(ColorPalette),
126+
ScreenshotSavePath = ScreenshotSavePath,
127+
CopyScreenshotToClipboard = CopyScreenshotToClipboard,
128+
PlayShutterSound = PlayShutterSound,
129+
OpenFolderAfterScreenshot = OpenFolderAfterScreenshot
100130
};
101131
}
102132
}

Src/GhostDraw/Core/GlobalKeyboardHook.cs

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Diagnostics;
1+
using System.Diagnostics;
22
using System.Runtime.InteropServices;
33
using Microsoft.Extensions.Logging;
44

@@ -17,6 +17,9 @@ public class GlobalKeyboardHook : IDisposable
1717
private const int VK_P = 0x50; // 80 - 'P' key for pen tool
1818
private const int VK_E = 0x45; // 69 - 'E' key for eraser tool
1919
private const int VK_F1 = 0x70; // 112 - 'F1' key for help
20+
private const int VK_S = 0x53; // 83 - 'S' key for screenshot (Ctrl+S only)
21+
private const int VK_LCONTROL = 0xA2; // 162 - Left Control key
22+
private const int VK_RCONTROL = 0xA3; // 163 - Right Control key
2023

2124
private readonly ILogger<GlobalKeyboardHook> _logger;
2225
private readonly LowLevelKeyboardProc _proc;
@@ -33,6 +36,7 @@ public class GlobalKeyboardHook : IDisposable
3336
public event EventHandler? LineToolPressed;
3437
public event EventHandler? EraserToolPressed;
3538
public event EventHandler? HelpPressed;
39+
public event EventHandler? ScreenshotFullPressed;
3640

3741
// NEW: Raw key events for recorder
3842
public event EventHandler<KeyEventArgs>? KeyPressed;
@@ -42,6 +46,10 @@ public class GlobalKeyboardHook : IDisposable
4246
private List<int> _hotkeyVKs = new() { 0xA2, 0xA4, 0x44 }; // Default: Ctrl+Alt+D
4347
private Dictionary<int, bool> _keyStates = new();
4448
private bool _wasHotkeyActive = false;
49+
private volatile bool _isControlPressed = false;
50+
51+
// Drawing mode state - used to determine if we should suppress keys
52+
private volatile bool _isDrawingModeActive = false;
4553

4654
public GlobalKeyboardHook(ILogger<GlobalKeyboardHook> logger)
4755
{
@@ -53,7 +61,7 @@ public GlobalKeyboardHook(ILogger<GlobalKeyboardHook> logger)
5361
foreach (var vk in _hotkeyVKs)
5462
_keyStates[vk] = false;
5563
}
56-
64+
5765
/// <summary>
5866
/// Configures the hotkey combination
5967
/// </summary>
@@ -171,6 +179,8 @@ private nint SetHook(LowLevelKeyboardProc proc)
171179

172180
private nint HookCallback(int nCode, nint wParam, nint lParam)
173181
{
182+
bool shouldSuppressKey = false;
183+
174184
try
175185
{
176186
if (nCode >= 0)
@@ -184,10 +194,19 @@ private nint HookCallback(int nCode, nint wParam, nint lParam)
184194
else
185195
KeyReleased?.Invoke(this, new KeyEventArgs(vkCode));
186196

197+
// Track Control key state (both left and right control keys)
198+
if (vkCode == VK_LCONTROL || vkCode == VK_RCONTROL)
199+
{
200+
_isControlPressed = isKeyDown;
201+
_logger.LogDebug("Control key ({Type}) {State}",
202+
vkCode == VK_LCONTROL ? "Left" : "Right",
203+
isKeyDown ? "PRESSED" : "RELEASED");
204+
}
205+
187206
// Check for ESC key press (emergency exit)
188207
if (vkCode == VK_ESCAPE && isKeyDown)
189208
{
190-
_logger.LogInformation("?? ESC pressed - emergency exit");
209+
_logger.LogInformation("🔴 ESC pressed - emergency exit");
191210
EscapePressed?.Invoke(this, EventArgs.Empty);
192211
}
193212

@@ -226,6 +245,32 @@ private nint HookCallback(int nCode, nint wParam, nint lParam)
226245
HelpPressed?.Invoke(this, EventArgs.Empty);
227246
}
228247

248+
// Check for Ctrl+S key press (full screenshot only - no snipping tool)
249+
if (vkCode == VK_S && isKeyDown && _isControlPressed)
250+
{
251+
_logger.LogInformation("====== CTRL+S DETECTED ======");
252+
_logger.LogInformation("Control key state: {IsControlPressed}", _isControlPressed);
253+
_logger.LogInformation("Drawing mode active: {IsDrawingModeActive}", _isDrawingModeActive);
254+
255+
_logger.LogInformation("Ctrl+S pressed - firing ScreenshotFullPressed event");
256+
ScreenshotFullPressed?.Invoke(this, EventArgs.Empty);
257+
_logger.LogInformation("ScreenshotFullPressed event fired, subscribers: {Count}",
258+
ScreenshotFullPressed?.GetInvocationList().Length ?? 0);
259+
260+
// Suppress Ctrl+S when drawing mode is active to prevent Windows Snipping Tool
261+
if (_isDrawingModeActive)
262+
{
263+
shouldSuppressKey = true;
264+
_logger.LogInformation("KEY WILL BE SUPPRESSED - Drawing mode is active");
265+
}
266+
else
267+
{
268+
_logger.LogInformation("KEY WILL NOT BE SUPPRESSED - Drawing mode is inactive");
269+
}
270+
271+
_logger.LogInformation("====== END CTRL+S HANDLING ======");
272+
}
273+
229274
// Track hotkey state
230275
if (_hotkeyVKs.Contains(vkCode))
231276
{
@@ -239,12 +284,12 @@ private nint HookCallback(int nCode, nint wParam, nint lParam)
239284
// Fire events on state changes
240285
if (allPressed && !_wasHotkeyActive)
241286
{
242-
_logger.LogInformation("?? HOTKEY PRESSED");
287+
_logger.LogInformation("🟢 HOTKEY PRESSED");
243288
HotkeyPressed?.Invoke(this, EventArgs.Empty);
244289
}
245290
else if (!allPressed && _wasHotkeyActive)
246291
{
247-
_logger.LogInformation("?? HOTKEY RELEASED");
292+
_logger.LogInformation("🟢 HOTKEY RELEASED");
248293
HotkeyReleased?.Invoke(this, EventArgs.Empty);
249294
}
250295

@@ -258,7 +303,15 @@ private nint HookCallback(int nCode, nint wParam, nint lParam)
258303
_logger.LogError(ex, "Exception in keyboard hook callback");
259304
}
260305

261-
// MUST ALWAYS call CallNextHookEx to allow other applications to process the hook
306+
// If we want to suppress the key, return 1 to block it from reaching other applications
307+
// Otherwise, call CallNextHookEx to allow other applications to process the hook
308+
if (shouldSuppressKey)
309+
{
310+
_logger.LogTrace("Key suppressed - not calling CallNextHookEx");
311+
return (nint)1;
312+
}
313+
314+
// MUST call CallNextHookEx for non-suppressed keys to allow other applications to process them
262315
return CallNextHookEx(_hookID, nCode, wParam, lParam);
263316
}
264317

@@ -280,6 +333,27 @@ public void Dispose()
280333
}
281334
}
282335

336+
/// <summary>
337+
/// Sets the drawing mode state. When active, certain keys (like Ctrl+S) will be suppressed
338+
/// to prevent Windows from intercepting them.
339+
/// </summary>
340+
public void SetDrawingModeActive(bool isActive)
341+
{
342+
var previousState = _isDrawingModeActive;
343+
_isDrawingModeActive = isActive;
344+
345+
if (previousState != isActive)
346+
{
347+
_logger.LogInformation("====== DRAWING MODE STATE CHANGED ======");
348+
_logger.LogInformation("Previous state: {PreviousState}, New state: {NewState}", previousState, isActive);
349+
_logger.LogInformation("Timestamp: {Timestamp}", DateTime.Now.ToString("HH:mm:ss.fff"));
350+
}
351+
else
352+
{
353+
_logger.LogDebug("Drawing mode state set to: {IsActive} (no change)", isActive);
354+
}
355+
}
356+
283357
// P/Invoke declarations
284358
private delegate nint LowLevelKeyboardProc(int nCode, nint wParam, nint lParam);
285359

Src/GhostDraw/Core/ServiceConfiguration.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public static ServiceProvider ConfigureServices()
5757
// Register application services (order matters for dependencies)
5858
services.AddSingleton<AppSettingsService>();
5959
services.AddSingleton<CursorHelper>();
60+
services.AddSingleton<ScreenshotService>();
6061

6162
// Register drawing tools
6263
services.AddSingleton<GhostDraw.Tools.PenTool>();

Src/GhostDraw/GhostDraw.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
<UseWPF>true</UseWPF>
99
<UseWindowsForms>true</UseWindowsForms>
1010
<ApplicationIcon>Assets\favicon.ico</ApplicationIcon>
11-
<Version>1.0.6</Version>
11+
<Version>1.0.7</Version>
12+
<EnableWindowsTargeting>true</EnableWindowsTargeting>
1213
</PropertyGroup>
1314

1415
<ItemGroup>

0 commit comments

Comments
 (0)