Repository navigation
Development Strip Editor History
Why a project has exactly one undo history, what one step carries, how an undo shows itself, and the interleavings to try before believing it.
Status — built.
MainWindowowns oneQUndoStackper project, keyed byProjectItem::uidand alive for the session; it is the active stack while either of that project's docks is in front.
MainWindow owns a QUndoGroup and, per project, one stack. setActiveStack() follows dock
visibility, so Ctrl+Z applies to whatever project the artist is looking at — but both of a project's
windows, the project dock and its strip editor, activate the same stack. What the window decides is
the project, never which half of the work is undoable.
The stacks are keyed by ProjectItem::uid and live for the session. A stack owned by a widget dies when
the widget does, and the project dock used to be destroyed on close — taking the history with it, and
taking with it the machinery an open strip editor was still sending edits to. Those edits were accepted
on screen and dropped. The dock now hides instead; only removing the project, or closing the workspace,
drops its history, and a command that outlives its project holds a QPointer and does nothing.
Why not one history per window. Because the obvious argument for splitting — Ctrl+Z must show its own effect; pressing it in the Output tab should not quietly rewind a balloon three screens away — buys that at a worse price. Reorder the pages, move a balloon, press Ctrl+Z in the project dock: with two histories the reorder is undone, not the balloon. The artist has just undone the second-to-last thing they did. That is harder to explain than "Ctrl+Z undid something in the other window", and no editor behaves that way.
Nobody else splits either. Blender has had one linear UndoStack per file since 2.8, whose entries are
typed steps — memfile, mesh, sculpt, text, image; the per-mode feel comes from a step encoding only
its own domain, not from a second stack. Image editors keep one history per image, shared by every view of it.
Photoshop, Krita and Figma keep one per document. VS Code is per buffer, which looks per-view only
because each view is a different document. Where a tool lets you undo something off-screen, it mitigates
by showing what will be undone — which is the section below.
Project::historyStepApplied(EditScope, uids) is emitted by the restores and by nothing else: an edit
is already on screen in the window it was made in, so only undo and redo need announcing. MainWindow
resolves the scope to a dock — the project dock, or that project's strip editor — and hands it to
showDockAttention(), which shows it, raises it, focuses it, and outlines it for about a second and a
half.
The scope is recorded with the step, by whoever made it (EditScope). It answers where to look, not
which half: a colour grade is a project step, but only the strip editor edits it, so undoing one stays in
the strip editor rather than bringing the project dock forward.
The outline appears only when that dock was not already the focused one. That condition is the whole effect: a flash on the window being looked at teaches nothing, and a flash that fires every time stops being read at all. The colour is the palette's highlight — "the colour this theme uses for look here" is exactly the question being asked — and the outline is a click-through child widget that fades itself out and deletes itself, so there is nothing to clean up and nothing to unset.
It lives beside DockTitleBar rather than inside the strip editor, because every dock in this
application has the same need and none of it is editor-specific.
One command per settled edit:
| Signal | Meaning | History |
|---|---|---|
changed() |
a control is moving — live preview | none |
committed() |
the control settled, or a discrete action fired | one step |
A per-keystroke or per-mouse-move history is unusable, so geometry is reported on mouse release and text edits are debounced. Every editor and panel spells the pair the same way.
A command holds the whole of its half before and after — OverlayState for a lettering step, the
library's project snapshot for a project one — rather than a description of what changed. Restoring a
known-good state is one code path however the edit was made; replaying a description is one path per kind
of edit, and the kinds keep arriving.
It holds the project as a QPointer, never a raw one: the stack outlives the widget, so a command whose
project has actually been removed does nothing rather than reaching into freed memory.
ProjectSnapshotCommand and OverlaySnapshotCommand share one stack and are undone in the order they
were made. What is typed is their reach, and that is where the value is: restoring the lettering does
not reload the project, restoring the project does not rewind the lettering, and neither command has to
know what the other did.
Making that true took work, because the library's project snapshot carries the overlays — they are part of the project:
-
A project step snapshots the library's project state (
fullSnapshot()) and no authoring records. On restore (applyProjectSnapshot()) it lifts the overlays out and puts them back, so undoing a page reorder cannot rewind every bubble to where it stood at the time of the reorder. -
A lettering step carries
OverlayState—stripOverlaysplus the authoring records, typed rather than serialised — and touches nothing else. The two halves of it are never split: an overlay says which file renders, its record says what that file contains.
Which half a step covers is decided by what the operation changes, not by which window triggered it: Clear text & bubbles is a button on the project dock and records a lettering step.
Two properties then hold for free:
- Deleting an input does not delete objects. They become unanchored and are preserved, so every editor command still has its subject. See Inputs and anchoring.
- Undoing an editor delete after its page has gone re-creates the object unanchored, which is correct rather than broken.
Nothing is cleared when the project changes structurally. That is a judgement, not a proof, so it comes with a list.
Dock attention says where; the selection says what. Undoing or redoing selects whatever the step touched and scrolls it into view, in the canvas and in the object list — a long chapter is exactly where an undone edit sits three screens away, and a selection that changes off-screen is no better than no selection at all.
The objects are found by diffing the two overlay states, in Project::restoreOverlayState(), rather
than being tracked per operation. So every kind of edit reports what it moved without any of them being
taught to, and a kind added later reports correctly on the day it is added. Records are compared
alongside placements: a text edit rewrites a bubble's asset, and reading that as "changed" through the
placement alone would rest on the hash having settled.
The signal is emitted before the state reaches the views, which is the part worth remembering. It
arms Editor::selectAfterFeed() and the feed that follows consumes the arming — the objects do not exist
in the editor until that feed builds them. It is the same shape as m_selectNewOverlay, which has always
had to wait for the round trip because the library mints a new object's uid; the difference is that this
one can name what it wants and that one cannot.
Two cases fall out rather than being handled:
- A step that removed everything it touched — an undone placement, a redone delete — finds nothing standing and clears the selection instead of pointing at a ghost.
-
A step whose objects are already in front of you moves nothing:
ensureVisible()andscrollToItem()are both no-ops on something already visible, the same "don't shout at someone who is already looking" rule the outline follows.
A list rather than a single uid, because one step can touch several objects. Only the first survivor is
selected, although the canvas can now hold several; the rest are carried, so selecting all of them is a
change in one place (ObjectController::setSource()).
Each row is one interleaving of the two step kinds, with the expected result stated so that a wrong one is recognisable rather than merely surprising.
| Do this | Expect | |
|---|---|---|
| 1 | Place a balloon on page 4 → delete input 4 → undo | the input comes back first (it was the last edit); the balloon is untouched and re-anchored |
| 2 | Delete input 4 → place a balloon → undo | the balloon goes, the page list does not move |
| 3 | Reorder pages → move a balloon → undo, undo | the balloon returns to where it was, then the page order does — last edit first, both times |
| 4 | Delete input 4 → delete the unanchored balloon → undo, undo | the balloon returns unanchored, then page 4 comes back and it re-anchors to it |
| 5 | With the strip editor focused, undo a page reorder | the project dock comes to the front and is outlined |
| 6 | With the strip editor focused, undo a balloon move | it undoes in place — no outline, because you are already looking at it |
| 7 | Close the project dock, letter for a while, undo several times | every step is there; the project dock reappears only when a step of its kind is undone |
| 8 | Replace file… on page 4 → undo/redo across it | nothing unanchors in either direction |
| 9 | Leave objects unanchored → Render | the gate appears; each of its four buttons does what it says |
| 10 | Move a balloon → scroll three pages away → undo | the strip scrolls back to it, and it is selected in the canvas and in the object stack |
| 11 | Place a balloon → undo | it is gone and nothing is selected — not a ghost row, not the previous selection |
| 12 | Edit a balloon's text → undo | the balloon is selected; OBJECT STATE shows the restored text |
Rows 3 and 4 are the ones that demonstrate the single history: everything undoes in the order it was done. Rows 5 and 6 demonstrate the attention rule, and 6 is the one that fails loudest if the "already focused" test is wrong. Rows 10–12 demonstrate the selection: 10 that it reaches off-screen, 11 that it lets go when there is nothing left to hold.
Every mutation is still fire-and-forget through a signal, becoming real only when MainWindow round-trips
it back — that part is unchanged and fine. What was not fine is that the round trip ended at a widget that
could be destroyed. With the project dock closed there was nowhere to push, so the edit was declined,
silently: the editor kept the edited state, showed it, and diverged from the model until the next feed.
The intent was right and the missing half was telling anyone. Tying the history and the editing machinery to the session rather than to a window removes the case instead of announcing it.
See also: Strip editor · Inputs and anchoring · Property groups
Platemaker GUI 1.4.3 · GPL-3.0 · Application repository · libplatemaker · Report an issue
Development pages synced to code commit 1331e01 (2026-10-04).
Overview
Manual
- Manual overview
- Installation
- Quick start
- Concepts
- Main window
- Workspaces
- Projects
- Canvas profiles
- Templates
- Output profiles
- Rendering
- File statuses
- Menu reference
- Troubleshooting
Comic production
Development
- Overview
- Environment
- Building
- Architecture
- Main window
- Workflows
- Releasing
- Contributing
- Windows hardening
Strip editor