BetaTry our live BPMN Workflow EditorSkip to content

Shortcuts & Actions API

Learn how to customize keyboard shortcuts and create custom actions for your NodeFlow editor. The shortcuts system uses Flutter's Actions and Shortcuts framework integrated with NodeFlow's action system.

Architecture Overview

The shortcuts system consists of three main components:

  1. NodeFlowAction - Defines executable operations
  2. NodeFlowShortcutManager - Maps keyboard shortcuts to actions
  3. NodeFlowKeyboardHandler - Integrates with Flutter's shortcuts system

All shortcuts are managed through the controller:

dart
final controller = NodeFlowController<MyData, dynamic>();
controller.shortcuts // Access the shortcuts manager

Customizing Shortcuts

Changing Existing Shortcuts

Reassign keyboard shortcuts to different actions:

dart
final controller = NodeFlowController<MyData, dynamic>();

// Change the fit-to-view shortcut from F to Q
controller.shortcuts.setShortcut(
  LogicalKeySet(LogicalKeyboardKey.keyQ),
  'fit_to_view',
);

// Add Cmd/Ctrl modifier
controller.shortcuts.setShortcut(
  LogicalKeySet(LogicalKeyboardKey.keyF, LogicalKeyboardKey.meta),
  'fit_to_view',
);

Removing Shortcuts

Remove keyboard shortcuts entirely:

dart
// Remove the F key shortcut for fit_to_view
controller.shortcuts.removeShortcut(
  LogicalKeySet(LogicalKeyboardKey.keyF),
);

Platform-Aware Shortcuts

Shortcuts automatically handle Cmd (macOS) and Ctrl (Windows/Linux). Both are registered by default:

dart
// This is already done by default for built-in actions
controller.shortcuts.setShortcut(
  LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.meta),
  'select_all_nodes',
);
controller.shortcuts.setShortcut(
  LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.control),
  'select_all_nodes',
);

Creating Custom Actions

Define Your Action

Create a class extending NodeFlowAction<T>:

dart
class SaveGraphAction<T> extends NodeFlowAction<T> {
  const SaveGraphAction()
    : super(
        id: 'save_graph',
        label: 'Save Graph',
        description: 'Save the current graph to file',
        category: 'File',
      );

  @override
  bool execute(NodeFlowController<T, dynamic> controller, BuildContext? context) {
    // Your save logic here
    final graph = controller.exportGraph();
    final json = graph.toJson((data) => data.toJson());

    // Example: Save to file
    // final file = File('graph.json');
    // await file.writeAsString(jsonEncode(json));

    // Show confirmation if context available
    if (context != null) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Graph saved successfully')),
      );
    }

    return true; // Return true if action succeeded
  }

  @override
  bool canExecute(NodeFlowController<T, dynamic> controller) {
    // Only allow saving if there are nodes
    return controller.nodes.isNotEmpty;
  }
}

Register the Action

Add your action to the shortcuts manager:

dart
final controller = NodeFlowController<MyData, dynamic>();

// Register the action
controller.shortcuts.registerAction(SaveGraphAction<MyData>());

// Assign keyboard shortcuts (both platforms)
controller.shortcuts.setShortcut(
  LogicalKeySet(LogicalKeyboardKey.keyS, LogicalKeyboardKey.meta),
  'save_graph',
);
controller.shortcuts.setShortcut(
  LogicalKeySet(LogicalKeyboardKey.keyS, LogicalKeyboardKey.control),
  'save_graph',
);

Use in Your App

The shortcut now works automatically in your editor:

dart
NodeFlowEditor(
  controller: controller,
  nodeBuilder: (context, node) => MyNodeWidget(node),
)

// Press Cmd+S (Mac) or Ctrl+S (Windows/Linux) to save!

NodeFlowAction API

Required Members

dart
abstract class NodeFlowAction<T> {
  const NodeFlowAction({
    required this.id,        // Unique identifier
    required this.label,     // Display name
    this.description,        // Optional description
    this.category = 'General', // For grouping in menus
  });

  /// Execute the action's operation
  bool execute(NodeFlowController<T, dynamic> controller, BuildContext? context);

  /// Check if action can currently be executed
  bool canExecute(NodeFlowController<T, dynamic> controller) => true;
}

Action Parameters

ParameterTypeDescription
idStringUnique identifier for the action (e.g., 'save_graph')
labelStringHuman-readable name shown in UI (e.g., 'Save Graph')
descriptionString?Optional tooltip text
categoryStringGroups actions in menus (e.g., 'File', 'Edit', 'View')

Execute Method

The execute method performs the action:

Parameters:

  • controller: The NodeFlowController to operate on
  • context: Optional BuildContext for showing dialogs/snackbars

Returns: bool - true if action succeeded, false otherwise

CanExecute Method

The canExecute method determines when an action is available:

dart
@override
bool canExecute(NodeFlowController<T, dynamic> controller) {
  // Example: Only enable when multiple nodes selected
  return controller.selectedNodeIds.length >= 2;
}

Common patterns:

  • Check for selection: controller.selectedNodeIds.isNotEmpty
  • Check node count: controller.nodes.length > 0
  • Check permissions: controller.enableNodeDeletion
  • Custom logic: Any boolean condition

NodeFlowShortcutManager API

Access through controller.shortcuts:

Methods

registerAction

Register a single action:

dart
controller.shortcuts.registerAction(MyCustomAction<MyData>());

registerActions

Register multiple actions at once:

dart
controller.shortcuts.registerActions([
  SaveAction<MyData>(),
  LoadAction<MyData>(),
  ExportAction<MyData>(),
]);

setShortcut

Map a keyboard shortcut to an action:

dart
controller.shortcuts.setShortcut(
  LogicalKeySet(LogicalKeyboardKey.keyS, LogicalKeyboardKey.meta),
  'save_graph', // Action ID
);

removeShortcut

Remove a keyboard shortcut:

dart
controller.shortcuts.removeShortcut(
  LogicalKeySet(LogicalKeyboardKey.keyF),
);

getAction

Get an action by its ID:

dart
final action = controller.shortcuts.getAction('save_graph');
if (action != null) {
  // Use the action
}

getShortcutForAction

Get the keyboard shortcut for an action:

dart
final shortcut = controller.shortcuts.getShortcutForAction('save_graph');
// Returns LogicalKeySet or null

getActionsByCategory

Get all actions grouped by category:

dart
final actionsByCategory = controller.shortcuts.getActionsByCategory();

for (final entry in actionsByCategory.entries) {
  print('Category: ${entry.key}');
  for (final action in entry.value) {
    print('  - ${action.label}');
  }
}

searchActions

Search for actions by query:

dart
final results = controller.shortcuts.searchActions('align');
// Returns list of actions matching the query

Common Patterns

Quick Node Creation

Create actions for adding specific node types:

dart
class AddNodeAction<T> extends NodeFlowAction<T> {
  final String nodeType;
  final String label;

  const AddNodeAction({
    required String id,
    required this.nodeType,
    required this.label,
  }) : super(
         id: id,
         label: label,
         category: 'Nodes',
       );

  @override
  bool execute(NodeFlowController<T, dynamic> controller, BuildContext? context) {
    final viewport = controller.viewport;
    final centerX = viewport.x + (viewport.width / 2);
    final centerY = viewport.y + (viewport.height / 2);

    final node = Node<T>(
      id: '$nodeType-${DateTime.now().millisecondsSinceEpoch}',
      type: nodeType,
      position: Offset(centerX, centerY),
      size: Size(150, 80),
      data: /* your data */,
    );

    controller.addNode(node);
    return true;
  }
}

// Register with number shortcuts
controller.shortcuts.registerAction(
  AddNodeAction<MyData>(
    id: 'add_start_node',
    nodeType: 'start',
    label: 'Add Start Node',
  ),
);
controller.shortcuts.setShortcut(
  LogicalKeySet(LogicalKeyboardKey.digit1),
  'add_start_node',
);

Conditional Actions

Actions that are only available under certain conditions:

dart
class AlignNodesAction<T> extends NodeFlowAction<T> {
  final NodeAlignment alignment;

  const AlignNodesAction(this.alignment)
    : super(
        id: 'align_${alignment.name}',
        label: 'Align ${alignment.name}',
        category: 'Alignment',
      );

  @override
  bool execute(NodeFlowController<T, dynamic> controller, BuildContext? context) {
    controller.alignNodes(
      controller.selectedNodeIds.toList(),
      alignment,
    );
    return true;
  }

  @override
  bool canExecute(NodeFlowController<T, dynamic> controller) {
    // Only enable when 2+ nodes are selected
    return controller.selectedNodeIds.length >= 2;
  }
}

Actions with Dialogs

Actions that show dialogs for user input:

dart
class RenameNodeAction<T> extends NodeFlowAction<T> {
  const RenameNodeAction()
    : super(
        id: 'rename_node',
        label: 'Rename Node',
        category: 'Editing',
      );

  @override
  bool execute(NodeFlowController<T, dynamic> controller, BuildContext? context) {
    if (context == null) return false;

    final selectedIds = controller.selectedNodeIds;
    if (selectedIds.isEmpty) return false;

    final node = controller.getNode(selectedIds.first);
    if (node == null) return false;

    showDialog(
      context: context,
      builder: (context) => RenameNodeDialog(
        node: node,
        onRename: (newName) {
          // Update node with new name
          controller.updateNode(node.copyWith(data: /* updated data */));
        },
      ),
    );

    return true;
  }

  @override
  bool canExecute(NodeFlowController<T, dynamic> controller) {
    return controller.selectedNodeIds.length == 1;
  }
}

Async Actions

Actions that perform asynchronous operations:

dart
class ExportImageAction<T> extends NodeFlowAction<T> {
  const ExportImageAction()
    : super(
        id: 'export_image',
        label: 'Export as Image',
        category: 'File',
      );

  @override
  bool execute(NodeFlowController<T, dynamic> controller, BuildContext? context) {
    // Trigger async operation
    _exportToImage(controller, context);
    return true;
  }

  Future<void> _exportToImage(
    NodeFlowController<T, dynamic> controller,
    BuildContext? context,
  ) async {
    try {
      // Show loading indicator
      if (context != null) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Exporting...')),
        );
      }

      // Perform export (pseudo-code)
      final imageBytes = await _captureGraph(controller);
      await _saveToFile(imageBytes);

      // Show success message
      if (context != null) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Export successful!')),
        );
      }
    } catch (e) {
      if (context != null) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Export failed: $e')),
        );
      }
    }
  }
}

Programmatic Execution

Execute actions programmatically without keyboard input:

dart
// Execute an action directly
final action = controller.shortcuts.getAction('fit_to_view');
if (action != null && action.canExecute(controller)) {
  action.execute(controller, context);
}

// Execute by ID (simpler)
void executeAction(String actionId) {
  final action = controller.shortcuts.getAction(actionId);
  if (action?.canExecute(controller) ?? false) {
    action!.execute(controller, null);
  }
}

// Use in buttons
ElevatedButton(
  onPressed: () => executeAction('save_graph'),
  child: Text('Save'),
)

Introspection

Query the shortcuts system:

dart
// Get all shortcuts
final allShortcuts = controller.shortcuts.shortcuts;

// Get all actions
final allActions = controller.shortcuts.actions;

// Find actions by category
final fileActions = controller.shortcuts
    .getActionsByCategory()['File'] ?? [];

// Search for actions
final alignActions = controller.shortcuts.searchActions('align');

// Check if shortcut exists
final saveShortcut = controller.shortcuts.getShortcutForAction('save_graph');
if (saveShortcut != null) {
  print('Save shortcut: ${_formatShortcut(saveShortcut)}');
}

Built-in Action IDs

Reference for built-in actions you can customize or extend:

Selection

  • select_all_nodes - Select all nodes
  • invert_selection - Invert current selection
  • clear_selection - Clear selection

Editing

  • delete_selected - Delete selected items
  • duplicate_selected - Duplicate selected nodes
  • cut_selected - Cut selected nodes (not implemented)
  • copy_selected - Copy selected nodes (not implemented)
  • paste - Paste nodes (not implemented)
  • edit_annotation - Edit selected node (comment text or group title)
  • toggle_snapping - Toggle grid snapping
  • fit_to_view - Fit all nodes in viewport
  • fit_selected - Fit selected nodes in viewport
  • reset_zoom - Reset zoom to 100%
  • zoom_in - Zoom in
  • zoom_out - Zoom out

Arrangement

  • bring_to_front - Bring selected to front
  • send_to_back - Send selected to back
  • bring_forward - Bring selected forward one layer
  • send_backward - Send selected backward one layer

Alignment

  • align_top - Align selected nodes to top
  • align_bottom - Align selected nodes to bottom
  • align_left - Align selected nodes to left
  • align_right - Align selected nodes to right
  • align_horizontal_center - Align to horizontal center
  • align_vertical_center - Align to vertical center

Grouping (shortcuts registered, actions pending)

  • create_group - Create a group from selected nodes (not implemented)
  • ungroup_node - Ungroup selected group node (not implemented)

View

  • toggle_minimap - Toggle minimap visibility

General

  • cancel_operation - Cancel current operation

Best Practices

  1. Unique IDs: Use descriptive, unique action IDs (e.g., 'save_graph' not 'save')
  2. Categories: Group related actions in meaningful categories
  3. Can Execute: Always implement canExecute to disable actions when not applicable
  4. Error Handling: Handle errors gracefully in execute method
  5. Context Usage: Check for null context before showing dialogs/snackbars
  6. Return Values: Return true only when action actually succeeds
  7. Side Effects: Avoid side effects in canExecute - it's called frequently
  8. Platform Shortcuts: Register both Cmd and Ctrl variants for cross-platform support

See Also