Skip to content

Commit 0de1c62

Browse files
committed
feat: add line tool
1 parent 13787ad commit 0de1c62

15 files changed

Lines changed: 1209 additions & 167 deletions

Src/GhostDraw/App.xaml.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ protected override void OnStartup(StartupEventArgs e)
7474
_keyboardHook.HotkeyReleased += OnHotkeyReleased;
7575
_keyboardHook.EscapePressed += OnEscapePressed;
7676
_keyboardHook.ClearCanvasPressed += OnClearCanvasPressed;
77+
_keyboardHook.PenToolPressed += OnPenToolPressed;
78+
_keyboardHook.LineToolPressed += OnLineToolPressed;
79+
_keyboardHook.HelpPressed += OnHelpPressed;
7780
_keyboardHook.Start();
7881

7982
// Setup system tray icon
@@ -251,6 +254,57 @@ private void OnClearCanvasPressed(object? sender, EventArgs e)
251254
}
252255
}
253256

257+
private void OnPenToolPressed(object? sender, EventArgs e)
258+
{
259+
try
260+
{
261+
// Only switch to pen tool if drawing mode is active
262+
if (_drawingManager?.IsDrawingMode == true)
263+
{
264+
_logger?.LogInformation("P pressed - selecting pen tool");
265+
_drawingManager?.SetPenTool();
266+
}
267+
}
268+
catch (Exception ex)
269+
{
270+
_exceptionHandler?.HandleException(ex, "Pen tool handler");
271+
}
272+
}
273+
274+
private void OnLineToolPressed(object? sender, EventArgs e)
275+
{
276+
try
277+
{
278+
// Only switch to line tool if drawing mode is active
279+
if (_drawingManager?.IsDrawingMode == true)
280+
{
281+
_logger?.LogInformation("L pressed - selecting line tool");
282+
_drawingManager?.SetLineTool();
283+
}
284+
}
285+
catch (Exception ex)
286+
{
287+
_exceptionHandler?.HandleException(ex, "Line tool handler");
288+
}
289+
}
290+
291+
private void OnHelpPressed(object? sender, EventArgs e)
292+
{
293+
try
294+
{
295+
// Show help overlay if drawing mode is active
296+
if (_drawingManager?.IsDrawingMode == true)
297+
{
298+
_logger?.LogInformation("F1 pressed - showing help");
299+
_drawingManager?.ShowHelp();
300+
}
301+
}
302+
catch (Exception ex)
303+
{
304+
_exceptionHandler?.HandleException(ex, "Help pressed handler");
305+
}
306+
}
307+
254308
protected override void OnExit(ExitEventArgs e)
255309
{
256310
_logger?.LogInformation("Application exiting");

Src/GhostDraw/Core/AppSettings.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ public class AppSettings
3131
[JsonPropertyName("maxBrushThickness")]
3232
public double MaxBrushThickness { get; set; } = 20.0;
3333

34+
/// <summary>
35+
/// The currently active drawing tool
36+
/// </summary>
37+
[JsonPropertyName("activeTool")]
38+
public DrawTool ActiveTool { get; set; } = DrawTool.Pen;
39+
3440
/// <summary>
3541
/// Virtual key codes for the hotkey combination
3642
/// </summary>
@@ -86,6 +92,7 @@ public AppSettings Clone()
8692
BrushThickness = BrushThickness,
8793
MinBrushThickness = MinBrushThickness,
8894
MaxBrushThickness = MaxBrushThickness,
95+
ActiveTool = ActiveTool,
8996
HotkeyVirtualKeys = new List<int>(HotkeyVirtualKeys),
9097
LockDrawingMode = LockDrawingMode,
9198
LogLevel = LogLevel,

Src/GhostDraw/Core/DrawTool.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace GhostDraw.Core;
4+
5+
/// <summary>
6+
/// Available drawing tools
7+
/// </summary>
8+
[JsonConverter(typeof(JsonStringEnumConverter))]
9+
public enum DrawTool
10+
{
11+
/// <summary>
12+
/// Freehand drawing tool (default)
13+
/// </summary>
14+
Pen,
15+
16+
/// <summary>
17+
/// Straight line tool - click two points to draw a line
18+
/// </summary>
19+
Line
20+
}

Src/GhostDraw/Core/GlobalKeyboardHook.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ public class GlobalKeyboardHook : IDisposable
1313
// Only keep VK_ESCAPE constant (emergency exit)
1414
private const int VK_ESCAPE = 0x1B; // 27
1515
private const int VK_R = 0x52; // 82 - 'R' key for clear canvas
16+
private const int VK_L = 0x4C; // 76 - 'L' key for line tool
17+
private const int VK_P = 0x50; // 80 - 'P' key for pen tool
18+
private const int VK_F1 = 0x70; // 112 - 'F1' key for help
1619

1720
private readonly ILogger<GlobalKeyboardHook> _logger;
1821
private readonly LowLevelKeyboardProc _proc;
@@ -25,6 +28,9 @@ public class GlobalKeyboardHook : IDisposable
2528
public event EventHandler? HotkeyReleased;
2629
public event EventHandler? EscapePressed;
2730
public event EventHandler? ClearCanvasPressed;
31+
public event EventHandler? PenToolPressed;
32+
public event EventHandler? LineToolPressed;
33+
public event EventHandler? HelpPressed;
2834

2935
// NEW: Raw key events for recorder
3036
public event EventHandler<KeyEventArgs>? KeyPressed;
@@ -190,6 +196,27 @@ private nint HookCallback(int nCode, nint wParam, nint lParam)
190196
ClearCanvasPressed?.Invoke(this, EventArgs.Empty);
191197
}
192198

199+
// Check for L key press (line tool)
200+
if (vkCode == VK_L && isKeyDown)
201+
{
202+
_logger.LogDebug("L key pressed - line tool request");
203+
LineToolPressed?.Invoke(this, EventArgs.Empty);
204+
}
205+
206+
// Check for P key press (pen tool)
207+
if (vkCode == VK_P && isKeyDown)
208+
{
209+
_logger.LogDebug("P key pressed - pen tool request");
210+
PenToolPressed?.Invoke(this, EventArgs.Empty);
211+
}
212+
213+
// Check for F1 key press (help)
214+
if (vkCode == VK_F1 && isKeyDown)
215+
{
216+
_logger.LogDebug("F1 key pressed - help request");
217+
HelpPressed?.Invoke(this, EventArgs.Empty);
218+
}
219+
193220
// Track hotkey state
194221
if (_hotkeyVKs.Contains(vkCode))
195222
{

Src/GhostDraw/Core/ServiceConfiguration.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ public static ServiceProvider ConfigureServices()
5151
builder.AddSerilog(dispose: true);
5252
});
5353

54+
// Register settings store (file-based for production)
55+
services.AddSingleton<ISettingsStore, FileSettingsStore>();
56+
5457
// Register application services (order matters for dependencies)
5558
services.AddSingleton<AppSettingsService>();
5659
services.AddSingleton<CursorHelper>();

Src/GhostDraw/Helpers/CursorHelper.cs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,125 @@ public WpfCursor CreateColoredPencilCursor(string tipColorHex)
147147
}
148148
}
149149

150+
/// <summary>
151+
/// Creates a crosshair cursor with color indicator for the Line tool
152+
/// </summary>
153+
/// <param name="colorHex">Hex color for the line (e.g., "#FF0000")</param>
154+
/// <returns>Custom cursor</returns>
155+
public WpfCursor CreateLineCursor(string colorHex)
156+
{
157+
lock (_cursorLock)
158+
{
159+
if (_disposed)
160+
{
161+
_logger.LogWarning("CreateLineCursor called on disposed CursorHelper");
162+
return WpfCursors.Cross;
163+
}
164+
165+
try
166+
{
167+
_logger.LogDebug("Creating line cursor with color {Color}", colorHex);
168+
169+
// Destroy previous cursor handle to prevent leaks
170+
if (_currentCursorHandle != nint.Zero)
171+
{
172+
try
173+
{
174+
DestroyCursor(_currentCursorHandle);
175+
_logger.LogDebug("Destroyed previous cursor handle");
176+
}
177+
catch (Exception ex)
178+
{
179+
_logger.LogError(ex, "Failed to destroy previous cursor handle");
180+
}
181+
_currentCursorHandle = nint.Zero;
182+
}
183+
184+
// Create a bitmap for the cursor (32x32 pixels)
185+
int size = 32;
186+
using (Bitmap bitmap = new Bitmap(size, size))
187+
using (Graphics g = Graphics.FromImage(bitmap))
188+
{
189+
g.SmoothingMode = SmoothingMode.AntiAlias;
190+
g.Clear(Color.Transparent);
191+
192+
// Parse the line color
193+
Color lineColor = ColorTranslator.FromHtml(colorHex);
194+
195+
// Draw two circles with a line connecting them
196+
// Left circle is at the hotspot (where the mouse clicks)
197+
int circleRadius = 4;
198+
int circleSpacing = 20; // Increased spacing for better visibility
199+
200+
// Position left circle at the hotspot (6 pixels from left edge for visual balance)
201+
Point leftCircleCenter = new Point(6, size / 2);
202+
Point rightCircleCenter = new Point(6 + circleSpacing, size / 2);
203+
204+
// Draw connecting line with the active color
205+
using (Pen linePen = new Pen(lineColor, 2))
206+
{
207+
g.DrawLine(linePen, leftCircleCenter, rightCircleCenter);
208+
}
209+
210+
// Draw left circle (outline) - this is where the line starts
211+
using (Pen circlePen = new Pen(Color.White, 2))
212+
{
213+
g.DrawEllipse(circlePen,
214+
leftCircleCenter.X - circleRadius,
215+
leftCircleCenter.Y - circleRadius,
216+
circleRadius * 2,
217+
circleRadius * 2);
218+
}
219+
using (Pen circleOutline = new Pen(Color.Black, 1))
220+
{
221+
g.DrawEllipse(circleOutline,
222+
leftCircleCenter.X - circleRadius - 1,
223+
leftCircleCenter.Y - circleRadius - 1,
224+
circleRadius * 2 + 2,
225+
circleRadius * 2 + 2);
226+
}
227+
228+
// Draw right circle (outline)
229+
using (Pen circlePen = new Pen(Color.White, 2))
230+
{
231+
g.DrawEllipse(circlePen,
232+
rightCircleCenter.X - circleRadius,
233+
rightCircleCenter.Y - circleRadius,
234+
circleRadius * 2,
235+
circleRadius * 2);
236+
}
237+
using (Pen circleOutline = new Pen(Color.Black, 1))
238+
{
239+
g.DrawEllipse(circleOutline,
240+
rightCircleCenter.X - circleRadius - 1,
241+
rightCircleCenter.Y - circleRadius - 1,
242+
circleRadius * 2 + 2,
243+
circleRadius * 2 + 2);
244+
}
245+
246+
// Convert bitmap to cursor with hotspot at the left circle center
247+
nint hCursor = CreateCursorFromBitmap(bitmap, leftCircleCenter.X, leftCircleCenter.Y);
248+
249+
if (hCursor != nint.Zero)
250+
{
251+
_currentCursorHandle = hCursor;
252+
_logger.LogDebug("Successfully created line cursor (handle: {Handle})", hCursor);
253+
254+
return System.Windows.Interop.CursorInteropHelper.Create(new SafeCursorHandle(hCursor));
255+
}
256+
}
257+
258+
_logger.LogWarning("Failed to create line cursor, returning default");
259+
return WpfCursors.Cross;
260+
}
261+
catch (Exception ex)
262+
{
263+
_logger.LogError(ex, "Error creating line cursor, using default");
264+
return WpfCursors.Cross;
265+
}
266+
}
267+
}
268+
150269
private nint CreateCursorFromBitmap(Bitmap bitmap, int hotspotX, int hotspotY)
151270
{
152271
nint hIcon = nint.Zero;

0 commit comments

Comments
 (0)