Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## v1.0.13

### Added
- **Undo (Ctrl+Z) with Permanent Eraser Semantics**
- Press `Ctrl+Z` while drawing mode is active to undo the most recent **completed** action (pen stroke, line, rectangle, circle)
- Erased items are permanently deleted and are never restored by undo
- New `DrawingHistory` service tracks completed actions using stable element IDs
- Added unit tests covering undo behavior and eraser permanence

### Changed
- **Tool Completion Events**
- Tools report completed actions (pen on mouse-up; shapes on second click) so undo is per-action instead of per-mouse-move
- Eraser reports erased elements so history entries are marked removed
- **Keyboard Hook Handling for Ctrl+Z**
- Detects `Ctrl+Z` only when drawing mode is active
- Suppresses `Ctrl+Z` during drawing mode to prevent pass-through to underlying apps
- **History Reset Behavior**
- Undo history is cleared when clearing the canvas and when exiting drawing mode


## v1.0.12

### Added
Expand Down
2 changes: 1 addition & 1 deletion Installer/GhostDraw.Installer.wixproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="WixToolset.Sdk/4.0.5">
<PropertyGroup>
<Version Condition="'$(Version)' == ''">1.0.12</Version>
<Version Condition="'$(Version)' == ''">1.0.13</Version>
<OutputName>GhostDrawSetup-$(Version)</OutputName>
<OutputType>Package</OutputType>
<Platform>x64</Platform>
Expand Down
26 changes: 25 additions & 1 deletion Src/GhostDraw/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ protected override void OnStartup(StartupEventArgs e)
_keyboardHook.CircleToolPressed += OnCircleToolPressed;
_keyboardHook.HelpPressed += OnHelpPressed;
_keyboardHook.ScreenshotFullPressed += OnScreenshotFullPressed;
_keyboardHook.UndoPressed += OnUndoPressed;
_keyboardHook.Start();

// Setup system tray icon
Expand Down Expand Up @@ -367,7 +368,7 @@ private void OnScreenshotFullPressed(object? sender, EventArgs e)
_logger?.LogInformation("====== OnScreenshotFullPressed CALLED ======");
_logger?.LogInformation("DrawingManager null: {IsNull}", _drawingManager == null);
_logger?.LogInformation("DrawingManager.IsDrawingMode: {IsDrawingMode}", _drawingManager?.IsDrawingMode);

// Capture full screenshot if drawing mode is active
if (_drawingManager?.IsDrawingMode == true)
{
Expand All @@ -388,6 +389,29 @@ private void OnScreenshotFullPressed(object? sender, EventArgs e)
}
}

private void OnUndoPressed(object? sender, EventArgs e)
{
try
{
_logger?.LogInformation("Ctrl+Z pressed - undoing last action");

// Undo last action if drawing mode is active
if (_drawingManager?.IsDrawingMode == true)
{
_drawingManager?.UndoLastAction();
}
else
{
_logger?.LogDebug("Undo ignored - drawing mode is NOT active");
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Exception in OnUndoPressed");
_exceptionHandler?.HandleException(ex, "Undo pressed handler");
}
}

protected override void OnExit(ExitEventArgs e)
{
_logger?.LogInformation("Application exiting");
Expand Down
2 changes: 1 addition & 1 deletion Src/GhostDraw/Core/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public class AppSettings
/// </summary>
[JsonPropertyName("screenshotSavePath")]
public string ScreenshotSavePath { get; set; } = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
"GhostDraw");

/// <summary>
Expand Down
8 changes: 4 additions & 4 deletions Src/GhostDraw/Core/DrawTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,22 @@ public enum DrawTool
/// Freehand drawing tool (default)
/// </summary>
Pen,

/// <summary>
/// Straight line tool - click two points to draw a line
/// </summary>
Line,

/// <summary>
/// Eraser tool - removes drawing objects underneath the cursor
/// </summary>
Eraser,

/// <summary>
/// Rectangle tool - click two points to draw a rectangle
/// </summary>
Rectangle,

/// <summary>
/// Circle tool - click two points to draw a circle/ellipse
/// </summary>
Expand Down
35 changes: 24 additions & 11 deletions Src/GhostDraw/Core/GlobalKeyboardHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class GlobalKeyboardHook : IDisposable
private const int VK_C = 0x43; // 67 - 'C' key for circle tool
private const int VK_F1 = 0x70; // 112 - 'F1' key for help
private const int VK_S = 0x53; // 83 - 'S' key for screenshot (Ctrl+S only)
private const int VK_Z = 0x5A; // 90 - 'Z' key for undo (Ctrl+Z only)
private const int VK_LCONTROL = 0xA2; // 162 - Left Control key
private const int VK_RCONTROL = 0xA3; // 163 - Right Control key

Expand All @@ -41,6 +42,7 @@ public class GlobalKeyboardHook : IDisposable
public event EventHandler? CircleToolPressed;
public event EventHandler? HelpPressed;
public event EventHandler? ScreenshotFullPressed;
public event EventHandler? UndoPressed;

// NEW: Raw key events for recorder
public event EventHandler<KeyEventArgs>? KeyPressed;
Expand All @@ -51,7 +53,7 @@ public class GlobalKeyboardHook : IDisposable
private Dictionary<int, bool> _keyStates = new();
private bool _wasHotkeyActive = false;
private volatile bool _isControlPressed = false;

// Drawing mode state - used to determine if we should suppress keys
private volatile bool _isDrawingModeActive = false;

Expand All @@ -65,7 +67,7 @@ public GlobalKeyboardHook(ILogger<GlobalKeyboardHook> logger)
foreach (var vk in _hotkeyVKs)
_keyStates[vk] = false;
}

/// <summary>
/// Configures the hotkey combination
/// </summary>
Expand Down Expand Up @@ -184,7 +186,7 @@ private nint SetHook(LowLevelKeyboardProc proc)
private nint HookCallback(int nCode, nint wParam, nint lParam)
{
bool shouldSuppressKey = false;

try
{
if (nCode >= 0)
Expand All @@ -202,8 +204,8 @@ private nint HookCallback(int nCode, nint wParam, nint lParam)
if (vkCode == VK_LCONTROL || vkCode == VK_RCONTROL)
{
_isControlPressed = isKeyDown;
_logger.LogDebug("Control key ({Type}) {State}",
vkCode == VK_LCONTROL ? "Left" : "Right",
_logger.LogDebug("Control key ({Type}) {State}",
vkCode == VK_LCONTROL ? "Left" : "Right",
isKeyDown ? "PRESSED" : "RELEASED");
}

Expand Down Expand Up @@ -269,12 +271,12 @@ private nint HookCallback(int nCode, nint wParam, nint lParam)
_logger.LogInformation("====== CTRL+S DETECTED ======");
_logger.LogInformation("Control key state: {IsControlPressed}", _isControlPressed);
_logger.LogInformation("Drawing mode active: {IsDrawingModeActive}", _isDrawingModeActive);

_logger.LogInformation("Ctrl+S pressed - firing ScreenshotFullPressed event");
ScreenshotFullPressed?.Invoke(this, EventArgs.Empty);
_logger.LogInformation("ScreenshotFullPressed event fired, subscribers: {Count}",
_logger.LogInformation("ScreenshotFullPressed event fired, subscribers: {Count}",
ScreenshotFullPressed?.GetInvocationList().Length ?? 0);

// Suppress Ctrl+S when drawing mode is active to prevent Windows Snipping Tool
if (_isDrawingModeActive)
{
Expand All @@ -285,10 +287,21 @@ private nint HookCallback(int nCode, nint wParam, nint lParam)
{
_logger.LogInformation("KEY WILL NOT BE SUPPRESSED - Drawing mode is inactive");
}

_logger.LogInformation("====== END CTRL+S HANDLING ======");
}

// Check for Ctrl+Z key press (undo - only when drawing mode is active)
if (vkCode == VK_Z && isKeyDown && _isControlPressed && _isDrawingModeActive)
{
_logger.LogInformation("Ctrl+Z pressed - firing UndoPressed event");
UndoPressed?.Invoke(this, EventArgs.Empty);

// Suppress Ctrl+Z when drawing mode is active to prevent underlying apps from receiving it
shouldSuppressKey = true;
_logger.LogDebug("Ctrl+Z suppressed - drawing mode is active");
}

// Track hotkey state
if (_hotkeyVKs.Contains(vkCode))
{
Expand Down Expand Up @@ -328,7 +341,7 @@ private nint HookCallback(int nCode, nint wParam, nint lParam)
_logger.LogTrace("Key suppressed - not calling CallNextHookEx");
return (nint)1;
}

// MUST call CallNextHookEx for non-suppressed keys to allow other applications to process them
return CallNextHookEx(_hookID, nCode, wParam, lParam);
}
Expand Down Expand Up @@ -359,7 +372,7 @@ public void SetDrawingModeActive(bool isActive)
{
var previousState = _isDrawingModeActive;
_isDrawingModeActive = isActive;

if (previousState != isActive)
{
_logger.LogInformation("====== DRAWING MODE STATE CHANGED ======");
Expand Down
1 change: 1 addition & 0 deletions Src/GhostDraw/Core/ServiceConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public static ServiceProvider ConfigureServices()
services.AddSingleton<AppSettingsService>();
services.AddSingleton<CursorHelper>();
services.AddSingleton<ScreenshotService>();
services.AddSingleton<DrawingHistory>();

// Register drawing tools
services.AddSingleton<GhostDraw.Tools.PenTool>();
Expand Down
2 changes: 1 addition & 1 deletion Src/GhostDraw/GhostDraw.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>Assets\favicon.ico</ApplicationIcon>
<Version>1.0.12</Version>
<Version>1.0.13</Version>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>

Expand Down
38 changes: 19 additions & 19 deletions Src/GhostDraw/Helpers/CursorHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,8 @@ public WpfCursor CreateEraserCursor()
for (int i = 0; i < 3; i++)
{
int offset = i * 5;
g.DrawLine(texturePen,
eraserLeft + offset, eraserTop,
g.DrawLine(texturePen,
eraserLeft + offset, eraserTop,
eraserLeft + offset + 6, eraserTop + eraserHeight);
}
}
Expand Down Expand Up @@ -410,7 +410,7 @@ public WpfCursor CreateLineCursor(string colorHex)
// Left circle is at the hotspot (where the mouse clicks)
int circleRadius = 4;
int circleSpacing = 20; // Increased spacing for better visibility

// Position left circle at the hotspot (6 pixels from left edge for visual balance)
Point leftCircleCenter = new Point(6, size / 2);
Point rightCircleCenter = new Point(6 + circleSpacing, size / 2);
Expand All @@ -424,36 +424,36 @@ public WpfCursor CreateLineCursor(string colorHex)
// Draw left circle (outline) - this is where the line starts
using (Pen circlePen = new Pen(Color.White, 2))
{
g.DrawEllipse(circlePen,
leftCircleCenter.X - circleRadius,
leftCircleCenter.Y - circleRadius,
circleRadius * 2,
g.DrawEllipse(circlePen,
leftCircleCenter.X - circleRadius,
leftCircleCenter.Y - circleRadius,
circleRadius * 2,
circleRadius * 2);
}
using (Pen circleOutline = new Pen(Color.Black, 1))
{
g.DrawEllipse(circleOutline,
leftCircleCenter.X - circleRadius - 1,
leftCircleCenter.Y - circleRadius - 1,
circleRadius * 2 + 2,
g.DrawEllipse(circleOutline,
leftCircleCenter.X - circleRadius - 1,
leftCircleCenter.Y - circleRadius - 1,
circleRadius * 2 + 2,
circleRadius * 2 + 2);
}

// Draw right circle (outline)
using (Pen circlePen = new Pen(Color.White, 2))
{
g.DrawEllipse(circlePen,
rightCircleCenter.X - circleRadius,
rightCircleCenter.Y - circleRadius,
circleRadius * 2,
g.DrawEllipse(circlePen,
rightCircleCenter.X - circleRadius,
rightCircleCenter.Y - circleRadius,
circleRadius * 2,
circleRadius * 2);
}
using (Pen circleOutline = new Pen(Color.Black, 1))
{
g.DrawEllipse(circleOutline,
rightCircleCenter.X - circleRadius - 1,
rightCircleCenter.Y - circleRadius - 1,
circleRadius * 2 + 2,
g.DrawEllipse(circleOutline,
rightCircleCenter.X - circleRadius - 1,
rightCircleCenter.Y - circleRadius - 1,
circleRadius * 2 + 2,
circleRadius * 2 + 2);
}

Expand Down
24 changes: 24 additions & 0 deletions Src/GhostDraw/Managers/DrawingManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -447,4 +447,28 @@ public void CaptureFullScreenshot()
// Don't re-throw - not critical
}
}

/// <summary>
/// Undoes the last drawing action (called via Ctrl+Z)
/// </summary>
public void UndoLastAction()
{
try
{
if (_overlayWindow.IsVisible)
{
_logger.LogInformation("Undo last action (Ctrl+Z)");
_overlayWindow.UndoLastAction();
}
else
{
_logger.LogDebug("UndoLastAction ignored - overlay not visible");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to undo last action");
// Don't re-throw - not critical
}
}
}
6 changes: 3 additions & 3 deletions Src/GhostDraw/Services/AppSettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ private AppSettings LoadSettings()
if (settings != null)
{
_logger.LogInformation("Settings loaded successfully");

// Save to ensure latest format
_settingsStore.Save(settings);
return settings;
Expand Down Expand Up @@ -280,8 +280,8 @@ public void SetActiveTool(DrawTool tool)
/// </summary>
public DrawTool ToggleTool()
{
var newTool = _currentSettings.ActiveTool == DrawTool.Pen
? DrawTool.Line
var newTool = _currentSettings.ActiveTool == DrawTool.Pen
? DrawTool.Line
: DrawTool.Pen;
SetActiveTool(newTool);
return newTool;
Expand Down
Loading