Skip to content

Undo and redo ​

Use controller.edits.undo() and controller.edits.redo() for document history. The diagram has one history; text editing, shape manipulation, connector changes and host commands do not maintain separate undo stacks. Each entry records the controller that made it, and undo() and redo() replay only that controller's own entries: an agent's edits stay when the user undoes, and the reverse.

History retains at most 50 committed commands across undo and redo together. Recording a new command evicts the oldest retained command when the budget is full. Undo and redo transfer entries without increasing this budget. A committed gesture or interaction lease counts as one command; pointer samples do not consume separate entries. The limit applies across participant branches, not separately to each branch.

On this page ​

CategoryWhat you can do
Undo and redoRead availability and undo or redo an edit.
Atomic editsCombine related changes into one operation.
Live dataApply edits that mirror live data without undo entries.
Restored contentUnderstand what an undo action changes.
Edit groupingKeep each user action as one history entry.
Permissions and eventsApply admission rules and observe completed history actions.

Controller API ​

MemberType / signatureBehavior
canUndobool get canUndoWhether this controller has an undo entry. Observable. This is availability, not a permission check.
canRedobool get canRedoWhether this controller has a redo entry. Observable. This is availability, not a permission check.
undo()EditResult undo()Restore the changed elements of this controller's latest entry, and its selection, after admission.
redo()EditResult redo()Reapply this controller's latest undone entry after admission.

undo() and redo() take no arguments. With an empty corresponding stack they return an allowed result without restoring anything. Commit or cancel this controller's active interaction before calling either method; neither may be called inside batch.

Return value ​

EditResult memberMeaning
bool allowedThe operation passed admission. An allowed no-op does not mean content changed.
DiagramDenialCode? codeCategory when denied, such as conflict when another controller is editing a restored element.
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. Undo restores only the elements its entry changed, so later edits by other controllers to other elements remain. If another controller has since changed those same elements, undo returns a conflict denial instead of overwriting them.

dart
final result = controller.edits.undo();
if (!result.allowed) {
  // Present result.reason in the host application's UI.
}

Keeping toolbar state current ​

dart
Observer(
  builder: (_) => Row(
    children: [
      IconButton(
        onPressed: controller.canUndo ? () => controller.edits.undo() : null,
        icon: const Icon(Icons.undo),
      ),
      IconButton(
        onPressed: controller.canRedo ? () => controller.edits.redo() : null,
        icon: const Icon(Icons.redo),
      ),
    ],
  ),
);

canUndo and canRedo are observable: the Observer (from package:flutter_mobx) rebuilds when either changes. Schedule follow-up edits outside the builder.

Atomic editing ​

Each verb is one validated operation. Verbs such as move and setStyle update multiple elements as one undo action. Use batch to combine several verbs into one undo step:

dart
final result = controller.edits.batch('Add pair', () {
  final a = controller.createShape(Shapes.rectangle, at: DiagramPoint.zero);
  final b = controller.createShape(Shapes.rectangle, at: const DiagramPoint(200, 0));
  controller.edits.connect(a, b);
});

batch runs on a cumulative interaction lease: each verb applies on top of the last, and the batch commits one entry. The first admission denial withdraws every earlier change and is returned. See batch.

For an edit that spans several calls, such as a panel that applies changes over time, open the cumulative lease yourself:

dart
final lease = controller.beginInteraction('Restyle', cumulative: true);
lease.perform([id], 'setOpacity', {'opacity': .5});
lease.perform([id], 'setStyle', {'strokeWidth': 2.0});
lease.commit(); // one undo entry; lease.cancel() restores the start

A cumulative lease yields: an unrelated edit, an undo or a new interaction commits it first. lease.isActive reports whether it is still open. There are no separate history groups.

A nested batch joins the outermost one and takes its history setting.

Edits without history ​

Edits that mirror live data, such as a telemetry feed updating a charger's kW, are not the user's intent and do not belong in undo. Pass history: false:

dart
controller.edits.batch('Telemetry', () {
  controller.edits.setText('charger', '42 kW', slot: 'body');
  controller.edits.setStyle(ids: ['charger'], fill: const DiagramColor(0xFFFFE9C7));
  controller.edits.setElementBounds(
    'charger-load',
    const DiagramRect.fromLTWH(0, 100, 126, 8),
    motion: Motion.smooth,
  );
}, history: false);
AspectBehavior
PipelineThe same as any batch: locks, rules, resolution, one publication and one diagram.changes event. A denial withdraws the whole batch.
Undo and redoNo entry for any controller; the redo stack is kept.
Change eventDiagramDocumentChange.history is false: skip autosave or dirty-marking for it.
SelectionUnchanged.
During an interactionRuns while this or another controller drags or types. It applies to the committed document; the interaction keeps the fields it is changing, such as a dragged frame, and shows the live values of the others. Another controller's preview does not deny it.
Any controllerA person's or an agent's controller can apply live data.

Earlier entries stay correct. When a live edit changes an element, every history entry that changed the same element, of every controller, is rebased field by field: undo restores the fields that entry changed and keeps the live values of the others. Fields are an element's placement and flags, each style property, each shape value, each connector route option and each paragraph of a text story; ports, connector labels and point lists are single fields.

A later live write to the same field wins. If the user changes a fill, a live update changes the text and the user presses Undo, the fill is restored and the live text stays. If both changed the text, undo leaves the live text. A live edit that deletes an element, or reorders elements, makes an older entry depending on it a conflict, as another controller's edit would.

See Live data for connecting a feed.

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 motion.
Options, such as grids and snapping, canvas bounds and fontsCurrent values are retained.
Active tool and default connector routeCurrent 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 same-content replacements and a lease or batch that returns to its starting content. Selection-only changes are transient. An unchanged verb 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 lease restores the redo branch that existed before it.

Editing granularity ​

OperationUndo unit
One verb, on one or more elementsOne entry.
Continuous move, resize or other editor gestureA gesture lease: live previews followed by one committed entry. Cancellation restores the baseline.
beginInteraction(label) leaseEach perform, performAll or preview starts over from the baseline; commit() records one entry, cancel() restores the start. While active, the previewed elements are leased to this controller.
beginInteraction(label, cumulative: true) leaseEach preview applies on top of the last; commit() records one entry.
IME compositionA cumulative lease, committed as one entry when composition finishes.
Ordinary text edits outside compositionEach text verb. No general time-based typing coalescing is exposed.
batch(label, build)Every verb in build, committed as one entry.
batch(label, build, history: false)No entry. Existing entries are rebased onto it.

Permissions and events ​

The beforeChange rule evaluates verbs, lease 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 beforeChange rule.

diagram.changes receives one completed nonempty edit from any controller, including undo/redo and edits without history, whose history is false. It excludes previews, cancellation, camera movement and selection-only changes. Read the controller's observable document inside an Observer when the host needs provisional state. See rules and changes 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 stays in the diagram’s memory and is excluded from document JSON. Use canUndo and canRedo for toolbar state. Stack inspection, limits, clearing and persistence are not exposed. Disposing a controller that joined a diagram preserves the shared history.