Skip to content

Add Ctrl+Z undo for completed drawing actions with permanent eraser semantics#33

Merged
RuntimeRascal merged 5 commits intomainfrom
copilot/add-undo-last-drawing
Dec 13, 2025
Merged

Add Ctrl+Z undo for completed drawing actions with permanent eraser semantics#33
RuntimeRascal merged 5 commits intomainfrom
copilot/add-undo-last-drawing

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Dec 13, 2025

Implements Ctrl+Z undo that removes the most recent completed drawing action (pen stroke, line, rectangle, or circle). Erased items are permanently deleted and never restored by undo.

Implementation

DrawingHistory Service

  • Stack-based undo with GUID-tagged UIElements via FrameworkElement.Tag
  • O(1) eraser lookups via Dictionary<Guid, HistoryEntry>
  • IsRemoved flag prevents undoing erased elements
  • Cleared on canvas clear (R key) and drawing mode exit

Tool Integration

  • ActionCompleted event fires on completion: pen MouseUp, shape second-click
  • ElementErased event marks history entries as removed
  • All tools except Eraser fire ActionCompleted

Keyboard Hook

  • VK_Z detection only when _isDrawingModeActive is true
  • Suppresses Ctrl+Z to prevent passthrough to underlying apps
  • Fast execution (<5ms), exception-safe, always calls CallNextHookEx

Example

// Tool fires event on completion
public void OnMouseUp(Point position, Canvas canvas)
{
    if (_currentStroke != null)
    {
        ActionCompleted?.Invoke(this, new DrawingActionCompletedEventArgs(_currentStroke));
        _currentStroke = null;
    }
}

// History tracks with stable ID
public void RecordAction(UIElement element)
{
    if (element is FrameworkElement fe)
    {
        var id = Guid.NewGuid();
        fe.Tag = id;
        var entry = new HistoryEntry(id, element);
        _undoStack.Push(entry);
        _elementIdToEntry[id] = entry;
    }
}

// Eraser marks as removed, preventing undo restoration
public void RemoveFromHistory(UIElement element)
{
    if (element is FrameworkElement fe && fe.Tag is Guid id)
    {
        if (_elementIdToEntry.TryGetValue(id, out var entry))
        {
            entry.IsRemoved = true;
            _elementIdToEntry.Remove(id);
        }
    }
}

Scope Exclusions

  • Redo (Ctrl+Y) - tracked separately
  • Undo history size limits
  • Visual feedback for undo operations
Original prompt

This section details on the original issue you should resolve

<issue_title>Feature: Undo last drawing with Ctrl+Z (eraser permanent)</issue_title>
<issue_description>## Summary
Add Undo Last Drawing using Ctrl+Z while drawing mode is active (overlay visible / locked). Undo removes the most recently completed drawing action across all tools (Pen strokes, Lines, Rectangles, Circles).

This is a more specific follow-up to #9 (which covers Z/X undo/redo); this issue focuses on Ctrl+Z undo plus the required eraser permanence semantics.

Key Requirements

  • Ctrl+Z only while drawing mode is active.
    • When drawing mode is inactive and overlay is not open, the app should only listen for the drawing-mode activation hotkey (no other key handling).
  • Undo step = one completed drawing action:
    • Pen: one stroke (mouse down → mouse up).
    • Line/Rectangle/Circle: one completed shape (first click starts, second click completes).
  • Eraser is permanent:
    • Erased items must never be restored by undo.
    • When an item is erased, its history entry must be removed so later Ctrl+Z does not bring it back.
  • No regression: existing drawing tools, help overlay, screenshot, and hotkeys continue working.
  • Hook safety: hook callbacks stay fast (<5ms) and never throw; always CallNextHookEx.

Behavior Examples

  • Draw 5 pen strokes, then 2 rectangles, then 2 circles. Press Ctrl+Z 5 times.
    • Result: removes the 2 circles, 2 rectangles, and the last pen stroke → leaves the first 4 pen strokes.
  • If the eraser deletes a rectangle that was drawn earlier:
    • That rectangle is gone permanently.
    • Subsequent Ctrl+Z should not restore it.

Proposed Implementation (High-Level)

1) Keyboard handling

  • Add Ctrl+Z detection in GlobalKeyboardHook only when drawing mode is active.
  • Raise a new event (e.g., UndoPressed) and (optionally) suppress the keystroke while drawing mode is active so underlying apps don’t receive Ctrl+Z.

2) History model

Maintain an in-memory history stack of completed actions.

Suggested approach (works with existing "one drawing == one WPF shape"):

  • On completion of a tool action, record a HistoryEntry that references the UIElement that was created for that action.
  • Undo pops entries until it finds one that still exists and is not erased; removing that element from the canvas is the undo.

3) Eraser permanence

When eraser removes an element from the canvas:

  • Remove the corresponding HistoryEntry from the history data structure immediately.
  • Clear any redo stack (if added later) to keep semantics consistent.

Implementation detail that makes this reliable:

  • Assign each drawable element a stable id (e.g., a Guid) using FrameworkElement.Tag or an attached property.
  • Maintain a dictionary id -> node/entry so the eraser can remove the history entry in O(1).

4) Integration points

  • Tools should notify the overlay/history when an action is completed (not when it starts).
    • Pen stroke: record on mouse up.
    • Line/Rectangle/Circle: record on second click (completion).
  • Add a UndoLastAction() method on the overlay/history owner that removes the last action from the canvas.

Acceptance Criteria

  • Ctrl+Z removes the most recent completed drawing action while overlay active.
  • Ctrl+Z does nothing when overlay inactive (except activation hotkey still works normally).
  • Pen/Line/Rectangle/Circle all participate in history correctly.
  • Eraser deletions are permanent and never come back via Ctrl+Z.
  • Clearing canvas and exiting drawing mode resets history appropriately (define: history cleared on exit and on canvas clear).
  • Unit tests added for:
    • History stack behavior for mixed actions.
    • Eraser removing history entries.
    • Undo skipping already-erased/removed entries.

Out of Scope (for this issue)

</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 3 commits December 13, 2025 02:57
Co-authored-by: RuntimeRascal <2422222+RuntimeRascal@users.noreply.github.com>
Co-authored-by: RuntimeRascal <2422222+RuntimeRascal@users.noreply.github.com>
Co-authored-by: RuntimeRascal <2422222+RuntimeRascal@users.noreply.github.com>
Copilot AI changed the title [WIP] Add Ctrl+Z functionality to undo last drawing action Add Ctrl+Z undo for completed drawing actions with permanent eraser semantics Dec 13, 2025
Copilot AI requested a review from RuntimeRascal December 13, 2025 03:05
@RuntimeRascal RuntimeRascal marked this pull request as ready for review December 13, 2025 03:26
@RuntimeRascal RuntimeRascal merged commit 7bf4260 into main Dec 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Undo last drawing with Ctrl+Z (eraser permanent)

2 participants