Skip to content

Commit 6f25a39

Browse files
Merge pull request #23 from RuntimeRascal/copilot/add-rectangle-drawing-tool
Add Rectangle drawing tool with U key shortcut
2 parents 99c8c30 + 09c7f12 commit 6f25a39

File tree

16 files changed

+556
-12
lines changed

16 files changed

+556
-12
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ 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

88

9+
## v1.0.8
10+
11+
### Added
12+
- **Rectangle Tool** - Draw rectangles by clicking two points
13+
- Press `U` to activate Rectangle tool
14+
- First click sets one corner, second click finalizes the rectangle
15+
- Live preview updates as you move the mouse
16+
- Respects current color and thickness settings
17+
- Custom rectangle cursor with corner markers
18+
- Supports right-click color cycling and mouse wheel thickness adjustment
19+
- Works seamlessly with Eraser tool
20+
921
## v1.0.7
1022

1123
### Added
@@ -22,6 +34,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2234
- 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
2335
- Thread safety improvements with volatile field for update nesting level
2436

37+
### Changed
38+
- Snipping tool (`S` key) now properly exits drawing mode to allow user interaction
39+
- User must manually reactivate drawing mode after using snipping tool (press hotkey)
40+
2541
## v1.0.6
2642

2743
### Added

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.7</Version>
3+
<Version Condition="'$(Version)' == ''">1.0.8</Version>
44
<OutputName>GhostDrawSetup-$(Version)</OutputName>
55
<OutputType>Package</OutputType>
66
<Platform>x64</Platform>

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
## ✨ Features
2424

2525
### 🎨 **Drawing Tools**
26+
- **Multiple Drawing Tools** - Switch between Pen (freehand), Line (straight lines), Rectangle (boxes), and Eraser tools with keyboard shortcuts
2627
- **Customizable Color Palette** - Create your own color collection and cycle through them while drawing
2728
- **Variable Brush Thickness** - Adjust brush size from 1-100px with configurable min/max ranges
2829
- **Smooth Drawing** - High-performance rendering for fluid strokes

Src/GhostDraw/App.xaml.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ protected override void OnStartup(StartupEventArgs e)
7777
_keyboardHook.PenToolPressed += OnPenToolPressed;
7878
_keyboardHook.LineToolPressed += OnLineToolPressed;
7979
_keyboardHook.EraserToolPressed += OnEraserToolPressed;
80+
_keyboardHook.RectangleToolPressed += OnRectangleToolPressed;
8081
_keyboardHook.HelpPressed += OnHelpPressed;
8182
_keyboardHook.ScreenshotFullPressed += OnScreenshotFullPressed;
8283
_keyboardHook.Start();
@@ -307,6 +308,23 @@ private void OnEraserToolPressed(object? sender, EventArgs e)
307308
}
308309
}
309310

311+
private void OnRectangleToolPressed(object? sender, EventArgs e)
312+
{
313+
try
314+
{
315+
// Only switch to rectangle tool if drawing mode is active
316+
if (_drawingManager?.IsDrawingMode == true)
317+
{
318+
_logger?.LogInformation("U pressed - selecting rectangle tool");
319+
_drawingManager?.SetRectangleTool();
320+
}
321+
}
322+
catch (Exception ex)
323+
{
324+
_exceptionHandler?.HandleException(ex, "Rectangle tool handler");
325+
}
326+
}
327+
310328
private void OnHelpPressed(object? sender, EventArgs e)
311329
{
312330
try

Src/GhostDraw/Core/DrawTool.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,10 @@ public enum DrawTool
2121
/// <summary>
2222
/// Eraser tool - removes drawing objects underneath the cursor
2323
/// </summary>
24-
Eraser
24+
Eraser,
25+
26+
/// <summary>
27+
/// Rectangle tool - click two points to draw a rectangle
28+
/// </summary>
29+
Rectangle
2530
}

Src/GhostDraw/Core/GlobalKeyboardHook.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class GlobalKeyboardHook : IDisposable
1616
private const int VK_L = 0x4C; // 76 - 'L' key for line tool
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
19+
private const int VK_U = 0x55; // 85 - 'U' key for rectangle tool
1920
private const int VK_F1 = 0x70; // 112 - 'F1' key for help
2021
private const int VK_S = 0x53; // 83 - 'S' key for screenshot (Ctrl+S only)
2122
private const int VK_LCONTROL = 0xA2; // 162 - Left Control key
@@ -35,6 +36,7 @@ public class GlobalKeyboardHook : IDisposable
3536
public event EventHandler? PenToolPressed;
3637
public event EventHandler? LineToolPressed;
3738
public event EventHandler? EraserToolPressed;
39+
public event EventHandler? RectangleToolPressed;
3840
public event EventHandler? HelpPressed;
3941
public event EventHandler? ScreenshotFullPressed;
4042

@@ -238,6 +240,13 @@ private nint HookCallback(int nCode, nint wParam, nint lParam)
238240
EraserToolPressed?.Invoke(this, EventArgs.Empty);
239241
}
240242

243+
// Check for U key press (rectangle tool)
244+
if (vkCode == VK_U && isKeyDown)
245+
{
246+
_logger.LogDebug("U key pressed - rectangle tool request");
247+
RectangleToolPressed?.Invoke(this, EventArgs.Empty);
248+
}
249+
241250
// Check for F1 key press (help)
242251
if (vkCode == VK_F1 && isKeyDown)
243252
{

Src/GhostDraw/Core/ServiceConfiguration.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public static ServiceProvider ConfigureServices()
6363
services.AddSingleton<GhostDraw.Tools.PenTool>();
6464
services.AddSingleton<GhostDraw.Tools.LineTool>();
6565
services.AddSingleton<GhostDraw.Tools.EraserTool>();
66+
services.AddSingleton<GhostDraw.Tools.RectangleTool>();
6667

6768
services.AddSingleton<OverlayWindow>();
6869
services.AddSingleton<GlobalKeyboardHook>();

Src/GhostDraw/GhostDraw.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<UseWPF>true</UseWPF>
99
<UseWindowsForms>true</UseWindowsForms>
1010
<ApplicationIcon>Assets\favicon.ico</ApplicationIcon>
11-
<Version>1.0.7</Version>
11+
<Version>1.0.8</Version>
1212
<EnableWindowsTargeting>true</EnableWindowsTargeting>
1313
</PropertyGroup>
1414

Src/GhostDraw/Helpers/CursorHelper.cs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,108 @@ public WpfCursor CreateEraserCursor()
259259
}
260260
}
261261

262+
/// <summary>
263+
/// Creates a cursor for the Rectangle tool with color indicator
264+
/// </summary>
265+
/// <param name="colorHex">Hex color for the rectangle (e.g., "#FF0000")</param>
266+
/// <returns>Custom cursor</returns>
267+
public WpfCursor CreateRectangleCursor(string colorHex)
268+
{
269+
lock (_cursorLock)
270+
{
271+
if (_disposed)
272+
{
273+
_logger.LogWarning("CreateRectangleCursor called on disposed CursorHelper");
274+
return WpfCursors.Cross;
275+
}
276+
277+
try
278+
{
279+
_logger.LogDebug("Creating rectangle cursor with color {Color}", colorHex);
280+
281+
// Destroy previous cursor handle to prevent leaks
282+
if (_currentCursorHandle != nint.Zero)
283+
{
284+
try
285+
{
286+
DestroyCursor(_currentCursorHandle);
287+
_logger.LogDebug("Destroyed previous cursor handle");
288+
}
289+
catch (Exception ex)
290+
{
291+
_logger.LogError(ex, "Failed to destroy previous cursor handle");
292+
}
293+
_currentCursorHandle = nint.Zero;
294+
}
295+
296+
// Create a bitmap for the cursor (32x32 pixels)
297+
int size = 32;
298+
using (Bitmap bitmap = new Bitmap(size, size))
299+
using (Graphics g = Graphics.FromImage(bitmap))
300+
{
301+
g.SmoothingMode = SmoothingMode.AntiAlias;
302+
g.Clear(Color.Transparent);
303+
304+
// Parse the rectangle color
305+
Color rectColor = ColorTranslator.FromHtml(colorHex);
306+
307+
// Draw a small rectangle preview with corner markers
308+
int rectSize = 12;
309+
int rectLeft = 6;
310+
int rectTop = (size - rectSize) / 2;
311+
312+
// Draw rectangle outline with the active color
313+
using (Pen rectPen = new Pen(rectColor, 2))
314+
{
315+
g.DrawRectangle(rectPen, rectLeft, rectTop, rectSize, rectSize);
316+
}
317+
318+
// Draw corner markers (small filled squares at corners)
319+
int cornerSize = 3;
320+
using (SolidBrush cornerBrush = new SolidBrush(Color.White))
321+
{
322+
// Top-left corner (this is the hotspot)
323+
g.FillRectangle(cornerBrush, rectLeft - 1, rectTop - 1, cornerSize, cornerSize);
324+
// Top-right corner
325+
g.FillRectangle(cornerBrush, rectLeft + rectSize - 1, rectTop - 1, cornerSize, cornerSize);
326+
// Bottom-left corner
327+
g.FillRectangle(cornerBrush, rectLeft - 1, rectTop + rectSize - 1, cornerSize, cornerSize);
328+
// Bottom-right corner
329+
g.FillRectangle(cornerBrush, rectLeft + rectSize - 1, rectTop + rectSize - 1, cornerSize, cornerSize);
330+
}
331+
332+
// Draw black outlines for corner markers
333+
using (Pen cornerOutline = new Pen(Color.Black, 1))
334+
{
335+
g.DrawRectangle(cornerOutline, rectLeft - 1, rectTop - 1, cornerSize, cornerSize);
336+
g.DrawRectangle(cornerOutline, rectLeft + rectSize - 1, rectTop - 1, cornerSize, cornerSize);
337+
g.DrawRectangle(cornerOutline, rectLeft - 1, rectTop + rectSize - 1, cornerSize, cornerSize);
338+
g.DrawRectangle(cornerOutline, rectLeft + rectSize - 1, rectTop + rectSize - 1, cornerSize, cornerSize);
339+
}
340+
341+
// Convert bitmap to cursor with hotspot at top-left corner marker
342+
nint hCursor = CreateCursorFromBitmap(bitmap, rectLeft, rectTop);
343+
344+
if (hCursor != nint.Zero)
345+
{
346+
_currentCursorHandle = hCursor;
347+
_logger.LogDebug("Successfully created rectangle cursor (handle: {Handle})", hCursor);
348+
349+
return System.Windows.Interop.CursorInteropHelper.Create(new SafeCursorHandle(hCursor));
350+
}
351+
}
352+
353+
_logger.LogWarning("Failed to create rectangle cursor, returning default");
354+
return WpfCursors.Cross;
355+
}
356+
catch (Exception ex)
357+
{
358+
_logger.LogError(ex, "Error creating rectangle cursor, using default");
359+
return WpfCursors.Cross;
360+
}
361+
}
362+
}
363+
262364
/// <summary>
263365
/// Creates a crosshair cursor with color indicator for the Line tool
264366
/// </summary>

Src/GhostDraw/Managers/DrawingManager.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,31 @@ public void SetEraserTool()
297297
}
298298
}
299299

300+
/// <summary>
301+
/// Sets the active tool to Rectangle
302+
/// </summary>
303+
public void SetRectangleTool()
304+
{
305+
try
306+
{
307+
if (_overlayWindow.IsVisible)
308+
{
309+
_appSettings.SetActiveTool(DrawTool.Rectangle);
310+
_overlayWindow.OnToolChanged(DrawTool.Rectangle);
311+
_logger.LogInformation("Tool set to Rectangle");
312+
}
313+
else
314+
{
315+
_logger.LogDebug("SetRectangleTool ignored - overlay not visible");
316+
}
317+
}
318+
catch (Exception ex)
319+
{
320+
_logger.LogError(ex, "Failed to set rectangle tool");
321+
// Don't re-throw - not critical
322+
}
323+
}
324+
300325
/// <summary>
301326
/// Shows the help popup with keyboard shortcuts
302327
/// </summary>

0 commit comments

Comments
 (0)