Undo, redo and transactions
Use session.undo() and session.redo() for document history. The session and its headless DiagramStore share one history; text editing, shape manipulation, connector changes and host commands do not maintain separate undo stacks.
Session API
| Member | Type / signature | Behavior |
|---|---|---|
canUndo | bool get canUndo | Whether an undo entry exists. This is availability, not a permission check. |
canRedo | bool get canRedo | Whether a redo entry exists. This is availability, not a permission check. |
undo() | DiagramPolicyDecision undo() | Restore the preceding entry's document and selection after admission. |
redo() | DiagramPolicyDecision redo() | Restore the next entry's document and selection after admission. |
dispatch(transaction) | DiagramPolicyDecision dispatch(DiagramTransaction transaction) | Apply all steps as one admitted, reversible edit. |
addListener(listener) | void addListener(DiagramStateListener listener) | Observe live publications and read updated history availability. |
removeListener(listener) | void removeListener(DiagramStateListener listener) | Remove the same listener instance. |
undo() and redo() take no arguments. With an empty corresponding stack they return an allowed decision without restoring anything. Both first close an active history group. Commit or cancel an active pointer interaction before calling either method programmatically.
Return value
DiagramPolicyDecision member | Meaning |
|---|---|
bool allowed | The operation passed admission. An allowed no-op does not mean content changed. |
String? reason | Optional explanation when denied. |
void requireAllowed() | Throws DiagramChangeDenied if denied; otherwise returns normally. |
A denied restoration leaves its history cursor and current publication unchanged. If closing a preceding group is denied, that group is rolled back and undo/redo does not proceed.
dart
final decision = session.undo();
if (!decision.allowed) {
// Present decision.reason in the host application's UI.
}Keeping toolbar state current
dart
void onDrawingChanged(DiagramEditorState state) {
setState(() {
// Rebuild controls that read session.canUndo / session.canRedo.
});
}
// Register once when the host mounts.
session.addListener(onDrawingChanged);
// Remove when the host unmounts.
session.removeListener(onDrawingChanged);A DiagramStateListener has the signature void Function(DiagramEditorState state). Listeners can read the published state, scene and history availability together. They must not synchronously dispatch another command; schedule that work after notification.
DiagramTransaction
Use a transaction when several related changes must publish and undo together.
dart
DiagramTransaction({
required List<DiagramStep> steps,
DiagramSelection? selectionAfter,
String label = 'Edit diagram',
})| Property | Type | Default | Contract |
|---|---|---|---|
steps | List<DiagramStep> | Required | Ordered, unmodifiable copy. Each step receives the preceding step's document. |
selectionAfter | DiagramSelection? | null | Optional resulting selection. Otherwise retain the current selection, then map it against the resolved result. |
label | String | 'Edit diagram' | Human-readable operation label retained for history and change events. |
dart
final decision = session.dispatch(DiagramTransaction(
label: 'Move pair',
steps: [
MoveShapeStep(
elementId: firstId,
delta: const DiagramPoint(40, 0),
),
MoveShapeStep(
elementId: secondId,
delta: const DiagramPoint(40, 0),
),
],
));
decision.requireAllowed();Steps are evaluated and the complete candidate is validated and resolved before publication. If a step throws, validation fails, or admission denies the candidate, the transaction does not partially publish. Direct transaction.apply(state) only computes a candidate; use session.dispatch to perform an edit with validation, history and notifications.
DiagramStep
dart
abstract interface class DiagramStep {
DiagramDocument apply(DiagramDocument document);
}Custom steps return a new immutable document. They must not mutate the input or call back into the store to publish another edit. The store applies its normal validation, lock checks and host admission to their result.
| Built-in step | Constructor arguments | Effect |
|---|---|---|
InsertElementStep | Positional DiagramElement element | Insert one element; rejects an existing ID. |
InsertElementsStep | Positional List<DiagramElement> elements | Insert a batch; rejects duplicate or existing IDs. |
RemoveElementStep | Positional String elementId | Remove one existing element. Prefer the removal command when connected or owned elements need cleanup. |
RemoveElementsStep | Positional Set<String> elementIds | Remove the canonical removal closure, including connectors that would retain dangling bindings. |
ReplaceElementStep | Required named DiagramElement before, DiagramElement after | Replace an unchanged expected element; requires the same ID. |
MoveShapeStep | Required named String elementId, DiagramPoint delta | Translate an existing framed element. |
ResizeShapeStep | Required named String elementId, DiagramRect frame | Replace a framed element's bounds; width and height must be positive. |
RotateShapeStep | Required named String elementId, double rotation | Set a framed element's rotation in radians. |
ReplaceTextStoryStep | Required named String elementId, DiagramTextStory before, DiagramTextStory after | Replace the expected story within its text owner. |
Stale before values throw StateError rather than overwriting newer edits. For complete document replacement and its revision check, use persistence and import.
What undo restores
| State | History behavior |
|---|---|
| Document elements, stories, bindings and order | Restored from the entry's before/after snapshot. |
| Selection | Restored with the document and mapped to valid resolved targets. |
| Camera, zoom and viewport animation | Current navigation is retained. See navigation and animation. |
| Canvas configuration, grids and snapping | Current settings are retained. |
| Active tool and default connector router | Current choices are retained. |
| Environmental layout refresh | No document history entry. |
Restoration resolves geometry before publishing it with the document and selection. Revisions remain monotonic: undo restores prior content, not an old revision number.
An edit with unchanged content creates no history entry or completed-change event and preserves redo. This includes empty transactions, same-content replacements and a gesture or group that returns to its starting content. Selection-only changes are transient. An unchanged direct dispatch retains the document revision and scene; already-published previews can have advanced the revision even when the final net edit is empty.
A new content-changing transaction after undo clears the redo branch. Cancelling a grouped edit restores the redo branch that existed before that group.
Editing granularity
| Operation | Undo unit |
|---|---|
One dispatch with multiple steps | One entry for the complete transaction. |
| Continuous move, resize or other editor gesture | Live previews followed by one committed entry. Cancellation restores the baseline. |
| IME composition | Provisional story updates grouped into one operation when composition finishes. |
| Ordinary text edits outside composition | Each dispatched text transaction. No general time-based typing coalescing is exposed. |
| Explicit history group | Multiple dispatches sharing a group token, closed as one operation. |
Advanced: history groups
These methods currently live on session.store (or a headless DiagramStore). DrawingSession.dispatch does not accept a group token. Use one transaction for a synchronous batch; use a group when several successive publications belong to one operation.
| Method | Signature | Contract |
|---|---|---|
beginHistoryGroup | DiagramHistoryGroup beginHistoryGroup() | Close the previous group and obtain ownership of a new group. Requires no active interaction. |
dispatch | DiagramPolicyDecision dispatch(DiagramTransaction transaction, {DiagramHistoryGroup? historyGroup}) | With a current token, publish a provisional grouped edit; without one, close the group and dispatch an independent edit. |
ownsHistoryGroup | bool ownsHistoryGroup(DiagramHistoryGroup token) | Check whether the token still owns the group. |
endHistoryGroup | DiagramPolicyDecision endHistoryGroup(DiagramHistoryGroup token) | Admit the final change and close it. A stale token is an allowed no-op. |
cancelHistoryGroup | void cancelHistoryGroup(DiagramHistoryGroup token, {String label = 'Cancel grouped edit'}) | Restore the baseline and prior redo branch. A stale token is a no-op. The current implementation does not emit a cancellation event or use its label. |
DiagramHistoryGroup is an opaque token obtained from the store; it has no public constructor. Its read-only int operationId identifies this operation within the store lifetime. The first content-changing transaction supplies the retained group label.
An unrelated dispatch, undo/redo, new history group or pointer interaction ends ownership. Dispatching with an expired token throws StateError. Group ownership is not an exclusive lock across asynchronous host work: check ownership before continuing and submit a fresh intent when it has been superseded.
Advanced: interaction previews
Custom tools can use the same engine interaction boundary. Built-in canvas tools already manage this lifecycle.
| Member | Signature | Contract |
|---|---|---|
isInteracting | bool get isInteracting | Whether an interaction owns the preview baseline. |
interactionStart | DiagramEditorState? get interactionStart | Immutable baseline, or null when idle. |
beginInteraction | void beginInteraction(String label) | Close any history group and capture the baseline. |
previewInteraction | DiagramPolicyDecision previewInteraction(DiagramTransaction transaction) | Evaluate from the original baseline, not the previous preview. Requires an active interaction. |
commitInteraction | DiagramPolicyDecision commitInteraction() | Recheck admission and record one nonempty history entry. An idle call is an allowed no-op. |
cancelInteraction | void cancelInteraction() | Restore the baseline without a history entry. An idle call is a no-op. |
For cumulative movement, supply the total delta from gesture start on each preview. Applying an incremental pointer delta here would repeatedly restart from the baseline. Denied previews restore the baseline while retaining ownership; a later permitted preview can recover. Denied commits restore the baseline and end the interaction.
Permissions and events
configuration.hooks.beforeChange evaluates transactions, grouped previews, gesture previews and history restoration. Undo and redo use DiagramChangePhase.commit and origins undo / redo; they receive new operation IDs. Their restoration of previously admitted snapshots bypasses ordinary element-lock checks, but does not bypass the host admission hook.
configuration.hooks.onChange receives one completed nonempty edit, including undo/redo. It excludes previews, cancellation, camera movement and selection-only changes. Use session.addListener when the host needs provisional state. See events and hooks for proposal and event property tables.
Keyboard shortcuts
With the canvas or paragraph editor focused:
| Action | Shortcut |
|---|---|
| Undo | Command/Ctrl + Z |
| Redo | Command/Ctrl + Shift + Z, or Command/Ctrl + Y |
The paragraph editor closes composition before invoking command-based undo/redo and resynchronizes its editing buffer from canonical state.
Current scope
History is in-memory and owned by the store. The public session API does not currently expose stack enumeration, entry labels, a history limit, clearing history, persistent history serialization or collaborative per-author undo. canUndo and canRedo are the available toolbar state; document JSON does not include the history stacks. Disposing an owned session disposes its store and releases that history. A session borrowing a store leaves the owner's history intact.