Version: 1.0 Date: 2025-11-09 Status: Active Related Documents: Event Schema Reference | Tool Framework Architecture
This document provides the authoritative reference for human-readable undo/redo labels in WireTuner. These labels power the user interface for undo/redo menus, history panels, and keyboard shortcut hints.
Purpose:
- Define consistent naming conventions for tool operations
- Map tool operations to user-facing labels
- Provide integration guidance for UI surfaces (menus, panels, tooltips)
Key Design Principles:
- Human-Readable: Labels use plain English (e.g., "Move Rectangle" not "MoveObjectEvent")
- Consistent: Similar operations across tools use parallel naming (e.g., "Create Path", "Create Rectangle")
- Concise: Labels are 1-3 words maximum
- Action-Oriented: Start with verbs ("Move", "Create", "Delete", "Modify")
All undo labels follow this format:
<Verb> <Object> [Detail]
Components:
- Verb: Action performed (Create, Move, Delete, Modify, Add, Finish)
- Object: Target entity (Path, Rectangle, Anchor, Handle, Selection)
- Detail (optional): Additional context (e.g., "Move Rectangle 5px")
| Operation | Label | Format |
|---|---|---|
| Creating a new path | "Create Path" | Verb + Object |
| Moving a rectangle | "Move Rectangle" | Verb + Object |
| Adding an anchor to path | "Add Anchor" | Verb + Object |
| Modifying handle position | "Adjust Handle" | Verb + Object |
| Deleting selected objects | "Delete Selection" | Verb + Object |
The pen tool creates vector paths with anchors and Bezier curves.
| Operation | Label | Description | Group Lifecycle |
|---|---|---|---|
| Path creation (complete) | "Create Path" | Entire path creation from first anchor to finish/close | StartGroupEvent → [anchors...] → EndGroupEvent |
| Path cancellation | (no label) | Canceled paths do not emit labels (no FinishPathEvent) | StartGroupEvent → EndGroupEvent (no completion) |
Implementation Note: The pen tool uses a single undo group for the entire path creation workflow. The label "Create Path" is emitted when the path is finished (Enter key, double-click, or click on first anchor). If the path is canceled (Escape key), no label is stored since the operation was incomplete.
Example:
// In pen_tool.dart
final groupId = telemetry.startUndoGroup(
toolId: 'pen',
label: 'Create Path',
);
// ... path creation logic ...
telemetry.endUndoGroup(
toolId: 'pen',
groupId: groupId,
label: 'Create Path',
);The selection tool selects and moves objects on the canvas.
| Operation | Label | Description | Group Lifecycle |
|---|---|---|---|
| Selecting objects | "Select Objects" | Single-click or marquee selection | Single event (no group) |
| Moving selected objects | "Move Objects" | Drag operation moving one or more objects | StartGroupEvent → [samples...] → EndGroupEvent |
| Deselecting all | "Deselect All" | Clear selection | Single event (no group) |
Implementation Note: Movement operations are sampled at 50ms intervals and grouped atomically. The label uses plural "Objects" to cover both single and multi-object moves.
Example:
// In selection_tool.dart (on pointer down)
final groupId = telemetry.startUndoGroup(
toolId: 'selection',
label: 'Move Objects',
);
// During drag (sampled)
telemetry.recordSample(
toolId: 'selection',
eventType: 'MoveObjectEvent',
);
// On pointer up
telemetry.endUndoGroup(
toolId: 'selection',
groupId: groupId,
label: 'Move Objects',
);The direct selection tool manipulates individual anchors and handles within paths.
| Operation | Label | Description | Group Lifecycle |
|---|---|---|---|
| Moving anchor position | "Move Anchor" | Drag operation moving a single anchor | StartGroupEvent → [samples...] → EndGroupEvent |
| Adjusting handle position | "Adjust Handle" | Drag operation modifying Bezier control point | StartGroupEvent → [samples...] → EndGroupEvent |
| Converting anchor type | "Convert Anchor" | Alt+click to toggle smooth/corner anchor | Single event (no group) |
Implementation Note: Anchor and handle drags are sampled and grouped. Anchor type conversions are discrete single events.
Example:
// In direct_selection_tool.dart (handle drag)
final groupId = telemetry.startUndoGroup(
toolId: 'direct_selection',
label: 'Adjust Handle',
);
// During drag
telemetry.recordSample(
toolId: 'direct_selection',
eventType: 'ModifyAnchorEvent',
);
// On pointer up
telemetry.endUndoGroup(
toolId: 'direct_selection',
groupId: groupId,
label: 'Adjust Handle',
);Shape tools create parametric shapes (rectangles, ellipses, polygons, stars).
| Tool | Operation | Label | Description | Group Lifecycle |
|---|---|---|---|---|
| Rectangle | Shape creation | "Create Rectangle" | Drag operation creating rectangle | StartGroupEvent → [samples...] → EndGroupEvent |
| Ellipse | Shape creation | "Create Ellipse" | Drag operation creating ellipse | StartGroupEvent → [samples...] → EndGroupEvent |
| Polygon | Shape creation | "Create Polygon" | Drag operation creating polygon | StartGroupEvent → [samples...] → EndGroupEvent |
| Star | Shape creation | "Create Star" | Drag operation creating star | StartGroupEvent → [samples...] → EndGroupEvent |
Implementation Note: Shape creation is a single drag operation from pointer down to pointer up. The preview is rendered during the drag, but the shape is only committed on pointer up.
Example:
// In rectangle_tool.dart
final groupId = telemetry.startUndoGroup(
toolId: 'rectangle',
label: 'Create Rectangle',
);
// During drag (samples for preview)
telemetry.recordSample(
toolId: 'rectangle',
eventType: 'CreateShapeEvent', // or preview events
);
// On pointer up (final shape)
telemetry.endUndoGroup(
toolId: 'rectangle',
groupId: groupId,
label: 'Create Rectangle',
);Undo labels are exposed via Provider for reactive UI binding.
Setup:
// In app shell (main.dart or shell widget)
ChangeNotifierProvider(
create: (_) => ToolTelemetry(
logger: logger,
config: EventCoreDiagnosticsConfig.debug(),
),
child: EditorShell(),
)Consumption:
// In menu or history panel widget
class UndoMenuItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final telemetry = context.watch<ToolTelemetry>();
final activeTool = context.watch<ToolManager>().activeToolId;
final label = activeTool != null
? telemetry.getLastCompletedLabel(activeTool)
: null;
return MenuItem(
label: label != null ? 'Undo $label' : 'Undo',
shortcut: 'Cmd+Z',
onPressed: () => undoController.undo(),
enabled: label != null,
);
}
}Edit Menu:
- Undo: "Undo <label>" (e.g., "Undo Create Path")
- Redo: "Redo <label>" (e.g., "Redo Move Objects")
- When no operation available: "Undo" / "Redo" (disabled)
Keyboard Shortcut Hints:
- macOS: "Cmd+Z" (undo), "Cmd+Shift+Z" (redo)
- Windows/Linux: "Ctrl+Z" (undo), "Ctrl+Shift+Z" (redo)
The history panel displays a chronological list of operations:
┌─────────────────────────────┐
│ History │
├─────────────────────────────┤
│ ► Create Rectangle │ ← Current position
│ Move Objects │
│ Adjust Handle │
│ Create Path │
│ Move Anchor │
└─────────────────────────────┘
Implementation:
- Current position marked with "►" indicator
- Clicking an entry triggers undo/redo to that position
- Labels fetched from
ToolTelemetry.allLastCompletedLabels
When implementing a new tool, follow this pattern:
1. Identify Undo Boundaries
Determine which operations should be atomic:
- Discrete actions (select, delete): Single events, no group
- Sampled operations (drag, move): Grouped with StartGroupEvent/EndGroupEvent
2. Choose Label
Select a human-readable label following the naming convention:
- Verb: Create, Move, Delete, Modify, Add, Adjust
- Object: Path, Anchor, Handle, Rectangle, etc.
3. Integrate Telemetry
// On operation start
final groupId = telemetry.startUndoGroup(
toolId: 'your_tool',
label: 'Your Label',
);
// During sampled events
telemetry.recordSample(
toolId: 'your_tool',
eventType: 'YourEventType',
);
// On operation complete
telemetry.endUndoGroup(
toolId: 'your_tool',
groupId: groupId,
label: 'Your Label',
);4. Test Label Display
Verify labels appear correctly in:
- Edit → Undo/Redo menu items
- History panel entries
- Keyboard shortcut tooltips
Accessing Labels:
// Single label (for menu)
final label = telemetry.getLastCompletedLabel(toolId);
// All labels (for history panel)
final allLabels = telemetry.allLastCompletedLabels;Reactivity:
// Use context.watch for automatic UI updates
final telemetry = context.watch<ToolTelemetry>();Null Handling:
- Labels are
nullif no operations completed - Display fallback text: "Undo" / "Redo" when label is null
- Disable menu items when label is null
Test Coverage:
- Label Registration: Verify labels are stored on endUndoGroup
- Label Retrieval: Verify getLastCompletedLabel returns correct value
- Label Persistence: Verify labels survive flush() calls
- Multi-Tool Isolation: Verify labels per tool don't interfere
Example Test:
test('endUndoGroup stores last completed label', () {
final telemetry = ToolTelemetry(
logger: Logger(),
config: EventCoreDiagnosticsConfig.debug(),
);
final groupId = telemetry.startUndoGroup(
toolId: 'pen',
label: 'Create Path',
);
telemetry.endUndoGroup(
toolId: 'pen',
groupId: groupId,
label: 'Create Path',
);
expect(telemetry.getLastCompletedLabel('pen'), equals('Create Path'));
});Test Coverage:
- Menu Label Update: Verify menu items update when operations complete
- History Panel Sync: Verify history panel reflects undo/redo navigation
- Provider Reactivity: Verify UI rebuilds on notifyListeners
Example Test:
testWidgets('undo menu displays last operation label', (tester) async {
await tester.pumpWidget(
ChangeNotifierProvider(
create: (_) => ToolTelemetry(...),
child: MaterialApp(home: EditorShell()),
),
);
// Perform operation
final telemetry = tester.read<ToolTelemetry>();
final groupId = telemetry.startUndoGroup(
toolId: 'pen',
label: 'Create Path',
);
telemetry.endUndoGroup(
toolId: 'pen',
groupId: groupId,
label: 'Create Path',
);
await tester.pump(); // Rebuild
// Verify menu label
expect(find.text('Undo Create Path'), findsOneWidget);
});In future iterations, labels may include contextual details:
- "Move Rectangle 5px" (distance)
- "Create Path (5 anchors)" (count)
- "Delete 3 Objects" (quantity)
This requires event payload analysis and templating support.
Labels will be localized in future releases:
final label = Localization.of(context).undoLabel(
operation: 'create_path',
);Future support for grouping multiple discrete actions:
- "Delete 3 Objects" (batch delete)
- "Align 5 Objects" (batch alignment)
Document Maintainer: WireTuner Architecture Team Last Updated: 2025-11-09 Next Review: After completion of I3.T9 (Tool Telemetry Implementation)