Skip to content

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

MemberType / signatureBehavior
canUndobool get canUndoWhether an undo entry exists. This is availability, not a permission check.
canRedobool get canRedoWhether 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 memberMeaning
bool allowedThe operation passed admission. An allowed no-op does not mean content changed.
String? reasonOptional 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',
})
PropertyTypeDefaultContract
stepsList<DiagramStep>RequiredOrdered, unmodifiable copy. Each step receives the preceding step's document.
selectionAfterDiagramSelection?nullOptional resulting selection. Otherwise retain the current selection, then map it against the resolved result.
labelString'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 stepConstructor argumentsEffect
InsertElementStepPositional DiagramElement elementInsert one element; rejects an existing ID.
InsertElementsStepPositional List<DiagramElement> elementsInsert a batch; rejects duplicate or existing IDs.
RemoveElementStepPositional String elementIdRemove one existing element. Prefer the removal command when connected or owned elements need cleanup.
RemoveElementsStepPositional Set<String> elementIdsRemove the canonical removal closure, including connectors that would retain dangling bindings.
ReplaceElementStepRequired named DiagramElement before, DiagramElement afterReplace an unchanged expected element; requires the same ID.
MoveShapeStepRequired named String elementId, DiagramPoint deltaTranslate an existing framed element.
ResizeShapeStepRequired named String elementId, DiagramRect frameReplace a framed element's bounds; width and height must be positive.
RotateShapeStepRequired named String elementId, double rotationSet a framed element's rotation in radians.
ReplaceTextStoryStepRequired named String elementId, DiagramTextStory before, DiagramTextStory afterReplace 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

StateHistory behavior
Document elements, stories, bindings and orderRestored from the entry's before/after snapshot.
SelectionRestored with the document and mapped to valid resolved targets.
Camera, zoom and viewport animationCurrent navigation is retained. See navigation and animation.
Canvas configuration, grids and snappingCurrent settings are retained.
Active tool and default connector routerCurrent choices are retained.
Environmental layout refreshNo 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

OperationUndo unit
One dispatch with multiple stepsOne entry for the complete transaction.
Continuous move, resize or other editor gestureLive previews followed by one committed entry. Cancellation restores the baseline.
IME compositionProvisional story updates grouped into one operation when composition finishes.
Ordinary text edits outside compositionEach dispatched text transaction. No general time-based typing coalescing is exposed.
Explicit history groupMultiple 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.

MethodSignatureContract
beginHistoryGroupDiagramHistoryGroup beginHistoryGroup()Close the previous group and obtain ownership of a new group. Requires no active interaction.
dispatchDiagramPolicyDecision dispatch(DiagramTransaction transaction, {DiagramHistoryGroup? historyGroup})With a current token, publish a provisional grouped edit; without one, close the group and dispatch an independent edit.
ownsHistoryGroupbool ownsHistoryGroup(DiagramHistoryGroup token)Check whether the token still owns the group.
endHistoryGroupDiagramPolicyDecision endHistoryGroup(DiagramHistoryGroup token)Admit the final change and close it. A stale token is an allowed no-op.
cancelHistoryGroupvoid 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.

MemberSignatureContract
isInteractingbool get isInteractingWhether an interaction owns the preview baseline.
interactionStartDiagramEditorState? get interactionStartImmutable baseline, or null when idle.
beginInteractionvoid beginInteraction(String label)Close any history group and capture the baseline.
previewInteractionDiagramPolicyDecision previewInteraction(DiagramTransaction transaction)Evaluate from the original baseline, not the previous preview. Requires an active interaction.
commitInteractionDiagramPolicyDecision commitInteraction()Recheck admission and record one nonempty history entry. An idle call is an allowed no-op.
cancelInteractionvoid 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:

ActionShortcut
UndoCommand/Ctrl + Z
RedoCommand/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.