Skip to content

Instantly share code, notes, and snippets.

@rodydavis
Created February 17, 2022 19:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rodydavis/e98bff13e4a75ee5fa2883c528336e64 to your computer and use it in GitHub Desktop.
Save rodydavis/e98bff13e4a75ee5fa2883c528336e64 to your computer and use it in GitHub Desktop.
Action manager example for flutter
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../model.dart';
abstract class AppAction extends Intent {
LogicalKeySet? get keys => null;
void action(AppModel model);
String get label;
void execute(BuildContext context) {
final model = context.read<AppModel>();
model.addAction(this);
}
bool get canUndo;
}
abstract class MultiNode extends AppAction {
final List<String> nodeIds;
MultiNode({required this.nodeIds});
}
abstract class SingleNode extends AppAction {
final String nodeId;
SingleNode({required this.nodeId});
}
class GroupAction extends AppAction {
GroupAction(this.actions);
final List<AppAction> actions;
@override
String get label => 'Group of actions';
@override
void action(AppModel model) {
for (final action in actions) {
action.action(model);
}
}
@override
bool get canUndo => false;
}
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:menubar/menubar.dart';
import 'package:widget_studio/src/commands/link_nodes.dart';
import '../model.dart';
import '../utils.dart';
import 'actions.dart';
import 'delete_nodes.dart';
import 'deselect_all.dart';
import 'duplicate_nodes.dart';
import 'group_nodes.dart';
import 'move_back.dart';
import 'move_front.dart';
import 'move_to_back.dart';
import 'move_to_front.dart';
import 'pan_canvas.dart';
import 'redo.dart';
import 'reset_canvas.dart';
import 'rotate_canvas.dart';
import 'select_all.dart';
import 'undo.dart';
import 'ungroup_nodes.dart';
import 'zoom_canvas.dart';
class ActionScope extends StatefulWidget {
const ActionScope({Key? key, required this.builder}) : super(key: key);
final Widget Function(
BuildContext context,
Map<LogicalKeyboardKey, bool> pressed,
) builder;
@override
State<ActionScope> createState() => _ActionScopeState();
}
class _ActionScopeState extends State<ActionScope> {
final keys = <LogicalKeyboardKey, bool>{};
List<AppAction> buildActions(AppModel model, Project project) {
final actions = <AppAction>[];
if (project.selection.length != project.nodeOrder.length) {
actions.add(SelectAll());
}
if (project.selection.isNotEmpty) {
if (project.selection.length == 1) {
// Single Node Actions
final nodeId = project.selection.first;
final node = project.nodes[nodeId];
if (node != null) {
actions.add(MoveToBack(nodeId));
actions.add(MoveBackwards(nodeId));
actions.add(MoveForwards(nodeId));
actions.add(MoveToFront(nodeId));
if (node is GroupBase) {
actions.add(UnGroup(nodeId));
}
}
} else {
if (project.selection.length == 2) {
actions.add(LinkNodes(project.selection));
}
// Multi Node Actions
actions.add(GroupNodes(project.selection));
}
// Any Node Action
actions.add(DeleteNodes(project.selection));
actions.add(DuplicateNodes(project.selection));
actions.add(DeselectAll());
}
// Canvas Actions
actions.add(PanRight());
actions.add(PanLeft());
actions.add(PanUp());
actions.add(PanDown());
actions.add(ZoomIn());
actions.add(ZoomOut());
actions.add(RotateLeft());
actions.add(RotateRight());
actions.add(ResetCanvas());
actions.add(Undo());
actions.add(Redo());
return actions;
}
void setMenu(AppModel model, List<AppAction> actions) {
// ignore: prefer_const_constructors
final edit = Submenu(label: 'Selection', children: []);
for (final a in actions) {
edit.children.add(MenuItem(
label: a.label,
shortcut: a.keys,
onClicked: () => model.addAction(a),
));
}
if (isDesktop) {
setApplicationMenu([edit]);
}
}
@override
Widget build(BuildContext context) {
final model = context.read<AppModel>();
return BlocBuilder<AppModel, AppState>(
builder: (context, state) {
final project = state.currentProject!;
final shortcuts = buildActions(model, project);
setMenu(model, shortcuts);
return Shortcuts(
key: Key('shortcut-scope-${state.hashCode}'),
shortcuts: <LogicalKeySet, Intent>{
for (final shortcut in shortcuts.where((e) {
return e.keys != null;
}).toList())
shortcut.keys!: shortcut
},
child: Actions(
key: Key('action-scope-${state.hashCode}'),
actions: {
for (final shortcut in shortcuts)
shortcut.runtimeType: CallbackAction(
onInvoke: (intent) => model.addAction(shortcut),
),
},
child: Focus(
autofocus: true,
child: widget.builder(context, keys),
onKey: (context, event) {
if (event is RawKeyDownEvent) {
keys[event.logicalKey] = true;
if (event.logicalKey == LogicalKeyboardKey.shiftLeft ||
event.logicalKey == LogicalKeyboardKey.shiftRight) {
keys[LogicalKeyboardKey.shift] = event.isShiftPressed;
}
if (event.logicalKey == LogicalKeyboardKey.controlLeft ||
event.logicalKey == LogicalKeyboardKey.controlRight) {
keys[LogicalKeyboardKey.control] = event.isControlPressed;
}
if (event.logicalKey == LogicalKeyboardKey.altLeft ||
event.logicalKey == LogicalKeyboardKey.altRight) {
keys[LogicalKeyboardKey.alt] = event.isAltPressed;
}
if (event.logicalKey == LogicalKeyboardKey.metaLeft ||
event.logicalKey == LogicalKeyboardKey.metaRight) {
keys[LogicalKeyboardKey.meta] = event.isMetaPressed;
}
} else if (event is RawKeyUpEvent) {
keys[event.logicalKey] = false;
}
return KeyEventResult.ignored;
},
),
),
);
},
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment