blob: 000198319a8411ac8bdb5dda25397c6db3ae8905 [file] [log] [blame]
Ian Hickson449f4a62019-11-27 15:04:02 -08001// Copyright 2014 The Flutter Authors. All rights reserved.
Greg Spencer387e2b02019-06-04 11:30:24 -07002// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
Ian Hickson449f4a62019-11-27 15:04:02 -08004
Greg Spencer0f68b462020-04-07 16:49:39 -07005import 'dart:collection';
6import 'dart:io';
7
Greg Spencer387e2b02019-06-04 11:30:24 -07008import 'package:flutter/foundation.dart';
9import 'package:flutter/material.dart';
10import 'package:flutter/services.dart';
11import 'package:flutter/widgets.dart';
12
Greg Spencer245d1b52019-11-26 18:32:34 -080013void main() {
Greg Spencer387e2b02019-06-04 11:30:24 -070014 runApp(const MaterialApp(
15 title: 'Actions Demo',
16 home: FocusDemo(),
17 ));
18}
19
Greg Spencer0f68b462020-04-07 16:49:39 -070020/// A class that can hold invocation information that an [UndoableAction] can
21/// use to undo/redo itself.
22///
23/// Instances of this class are returned from [UndoableAction]s and placed on
24/// the undo stack when they are invoked.
Greg Spencer21637312020-06-16 09:25:04 -070025class Memento extends Object with Diagnosticable {
Greg Spencer0f68b462020-04-07 16:49:39 -070026 const Memento({
27 @required this.name,
28 @required this.undo,
29 @required this.redo,
30 });
31
32 /// Returns true if this Memento can be used to undo.
33 ///
34 /// Subclasses could override to provide their own conditions when a command is
35 /// undoable.
36 bool get canUndo => true;
37
38 /// Returns true if this Memento can be used to redo.
39 ///
40 /// Subclasses could override to provide their own conditions when a command is
41 /// redoable.
42 bool get canRedo => true;
43
44 final String name;
45 final VoidCallback undo;
46 final ValueGetter<Memento> redo;
47
48 @override
49 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
50 super.debugFillProperties(properties);
51 properties.add(StringProperty('name', name));
52 properties.add(FlagProperty('undo', value: undo != null, ifTrue: 'undo'));
53 properties.add(FlagProperty('redo', value: redo != null, ifTrue: 'redo'));
54 }
55}
56
Greg Spencer387e2b02019-06-04 11:30:24 -070057/// Undoable Actions
58
59/// An [ActionDispatcher] subclass that manages the invocation of undoable
60/// actions.
61class UndoableActionDispatcher extends ActionDispatcher implements Listenable {
62 /// Constructs a new [UndoableActionDispatcher].
63 ///
64 /// The [maxUndoLevels] argument must not be null.
65 UndoableActionDispatcher({
66 int maxUndoLevels = _defaultMaxUndoLevels,
67 }) : assert(maxUndoLevels != null),
68 _maxUndoLevels = maxUndoLevels;
69
70 // A stack of actions that have been performed. The most recent action
71 // performed is at the end of the list.
Greg Spencer0f68b462020-04-07 16:49:39 -070072 final DoubleLinkedQueue<Memento> _completedActions = DoubleLinkedQueue<Memento>();
Greg Spencer387e2b02019-06-04 11:30:24 -070073 // A stack of actions that can be redone. The most recent action performed is
74 // at the end of the list.
Greg Spencer0f68b462020-04-07 16:49:39 -070075 final List<Memento> _undoneActions = <Memento>[];
Greg Spencer387e2b02019-06-04 11:30:24 -070076
77 static const int _defaultMaxUndoLevels = 1000;
78
79 /// The maximum number of undo levels allowed.
80 ///
81 /// If this value is set to a value smaller than the number of completed
82 /// actions, then the stack of completed actions is truncated to only include
83 /// the last [maxUndoLevels] actions.
84 int get maxUndoLevels => _maxUndoLevels;
85 int _maxUndoLevels;
86 set maxUndoLevels(int value) {
87 _maxUndoLevels = value;
88 _pruneActions();
89 }
90
91 final Set<VoidCallback> _listeners = <VoidCallback>{};
92
93 @override
94 void addListener(VoidCallback listener) {
95 _listeners.add(listener);
96 }
97
98 @override
99 void removeListener(VoidCallback listener) {
100 _listeners.remove(listener);
101 }
102
103 /// Notifies listeners that the [ActionDispatcher] has changed state.
104 ///
105 /// May only be called by subclasses.
106 @protected
107 void notifyListeners() {
Alexandre Ardhuin4f9b6cf2020-01-07 16:32:04 +0100108 for (final VoidCallback callback in _listeners) {
Greg Spencer387e2b02019-06-04 11:30:24 -0700109 callback();
110 }
111 }
112
113 @override
Greg Spencer0f68b462020-04-07 16:49:39 -0700114 Object invokeAction(Action<Intent> action, Intent intent, [BuildContext context]) {
115 final Object result = super.invokeAction(action, intent, context);
Greg Spencer387e2b02019-06-04 11:30:24 -0700116 print('Invoking ${action is UndoableAction ? 'undoable ' : ''}$intent as $action: $this ');
117 if (action is UndoableAction) {
Greg Spencer0f68b462020-04-07 16:49:39 -0700118 _completedActions.addLast(result as Memento);
Greg Spencer387e2b02019-06-04 11:30:24 -0700119 _undoneActions.clear();
120 _pruneActions();
121 notifyListeners();
122 }
123 return result;
124 }
125
126 // Enforces undo level limit.
127 void _pruneActions() {
128 while (_completedActions.length > _maxUndoLevels) {
Greg Spencer0f68b462020-04-07 16:49:39 -0700129 _completedActions.removeFirst();
Greg Spencer387e2b02019-06-04 11:30:24 -0700130 }
131 }
132
133 /// Returns true if there is an action on the stack that can be undone.
134 bool get canUndo {
135 if (_completedActions.isNotEmpty) {
Greg Spencer0f68b462020-04-07 16:49:39 -0700136 return _completedActions.first.canUndo;
Greg Spencer387e2b02019-06-04 11:30:24 -0700137 }
138 return false;
139 }
140
141 /// Returns true if an action that has been undone can be re-invoked.
142 bool get canRedo {
143 if (_undoneActions.isNotEmpty) {
Greg Spencer0f68b462020-04-07 16:49:39 -0700144 return _undoneActions.first.canRedo;
Greg Spencer387e2b02019-06-04 11:30:24 -0700145 }
146 return false;
147 }
148
149 /// Undoes the last action executed if possible.
150 ///
151 /// Returns true if the action was successfully undone.
152 bool undo() {
153 print('Undoing. $this');
154 if (!canUndo) {
155 return false;
156 }
Greg Spencer0f68b462020-04-07 16:49:39 -0700157 final Memento memento = _completedActions.removeLast();
158 memento.undo();
159 _undoneActions.add(memento);
Greg Spencer387e2b02019-06-04 11:30:24 -0700160 notifyListeners();
161 return true;
162 }
163
164 /// Re-invokes a previously undone action, if possible.
165 ///
166 /// Returns true if the action was successfully invoked.
167 bool redo() {
168 print('Redoing. $this');
169 if (!canRedo) {
170 return false;
171 }
Greg Spencer0f68b462020-04-07 16:49:39 -0700172 final Memento memento = _undoneActions.removeLast();
173 final Memento replacement = memento.redo();
174 _completedActions.add(replacement);
Greg Spencer387e2b02019-06-04 11:30:24 -0700175 _pruneActions();
176 notifyListeners();
177 return true;
178 }
179
180 @override
181 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
182 super.debugFillProperties(properties);
183 properties.add(IntProperty('undoable items', _completedActions.length));
184 properties.add(IntProperty('redoable items', _undoneActions.length));
Greg Spencer0f68b462020-04-07 16:49:39 -0700185 properties.add(IterableProperty<Memento>('undo stack', _completedActions));
186 properties.add(IterableProperty<Memento>('redo stack', _undoneActions));
Greg Spencer387e2b02019-06-04 11:30:24 -0700187 }
188}
189
190class UndoIntent extends Intent {
Greg Spencer0f68b462020-04-07 16:49:39 -0700191 const UndoIntent();
192}
193
194class UndoAction extends Action<UndoIntent> {
195 @override
Greg Spencer36767d02020-04-21 13:18:04 -0700196 bool isEnabled(UndoIntent intent) {
Greg Spencer0f68b462020-04-07 16:49:39 -0700197 final UndoableActionDispatcher manager = Actions.of(primaryFocus?.context ?? FocusDemo.appKey.currentContext, nullOk: true) as UndoableActionDispatcher;
198 return manager.canUndo;
199 }
Greg Spencer387e2b02019-06-04 11:30:24 -0700200
201 @override
Greg Spencer0f68b462020-04-07 16:49:39 -0700202 void invoke(UndoIntent intent) {
203 final UndoableActionDispatcher manager = Actions.of(primaryFocus?.context ?? FocusDemo.appKey.currentContext, nullOk: true) as UndoableActionDispatcher;
204 manager?.undo();
Greg Spencer387e2b02019-06-04 11:30:24 -0700205 }
206}
207
208class RedoIntent extends Intent {
Greg Spencer0f68b462020-04-07 16:49:39 -0700209 const RedoIntent();
210}
211
212class RedoAction extends Action<RedoIntent> {
213 @override
Greg Spencer36767d02020-04-21 13:18:04 -0700214 bool isEnabled(RedoIntent intent) {
Greg Spencer0f68b462020-04-07 16:49:39 -0700215 final UndoableActionDispatcher manager = Actions.of(primaryFocus.context, nullOk: true) as UndoableActionDispatcher;
216 return manager.canRedo;
217 }
Greg Spencer387e2b02019-06-04 11:30:24 -0700218
219 @override
Greg Spencer0f68b462020-04-07 16:49:39 -0700220 RedoAction invoke(RedoIntent intent) {
221 final UndoableActionDispatcher manager = Actions.of(primaryFocus.context, nullOk: true) as UndoableActionDispatcher;
222 manager?.redo();
223 return this;
Greg Spencer387e2b02019-06-04 11:30:24 -0700224 }
225}
226
Greg Spencer387e2b02019-06-04 11:30:24 -0700227/// An action that can be undone.
Greg Spencer0f68b462020-04-07 16:49:39 -0700228abstract class UndoableAction<T extends Intent> extends Action<T> {
Greg Spencer387e2b02019-06-04 11:30:24 -0700229 /// The [Intent] this action was originally invoked with.
230 Intent get invocationIntent => _invocationTag;
231 Intent _invocationTag;
232
233 @protected
234 set invocationIntent(Intent value) => _invocationTag = value;
235
Greg Spencer387e2b02019-06-04 11:30:24 -0700236 @override
237 @mustCallSuper
Greg Spencer0f68b462020-04-07 16:49:39 -0700238 void invoke(T intent) {
Greg Spencer02235652019-09-28 08:55:47 -0700239 invocationIntent = intent;
Greg Spencer387e2b02019-06-04 11:30:24 -0700240 }
Greg Spencer0f68b462020-04-07 16:49:39 -0700241}
Greg Spencer387e2b02019-06-04 11:30:24 -0700242
Greg Spencer0f68b462020-04-07 16:49:39 -0700243class UndoableFocusActionBase<T extends Intent> extends UndoableAction<T> {
Greg Spencer387e2b02019-06-04 11:30:24 -0700244 @override
Greg Spencer0f68b462020-04-07 16:49:39 -0700245 @mustCallSuper
246 Memento invoke(T intent) {
247 super.invoke(intent);
248 final FocusNode previousFocus = primaryFocus;
249 return Memento(name: previousFocus.debugLabel, undo: () {
250 previousFocus.requestFocus();
251 }, redo: () {
252 return invoke(intent);
253 });
Greg Spencer387e2b02019-06-04 11:30:24 -0700254 }
255}
256
Greg Spencer0f68b462020-04-07 16:49:39 -0700257class UndoableRequestFocusAction extends UndoableFocusActionBase<RequestFocusIntent> {
Greg Spencer387e2b02019-06-04 11:30:24 -0700258 @override
Greg Spencer0f68b462020-04-07 16:49:39 -0700259 Memento invoke(RequestFocusIntent intent) {
260 final Memento memento = super.invoke(intent);
261 intent.focusNode.requestFocus();
262 return memento;
Greg Spencer387e2b02019-06-04 11:30:24 -0700263 }
264}
265
266/// Actions for manipulating focus.
Greg Spencer0f68b462020-04-07 16:49:39 -0700267class UndoableNextFocusAction extends UndoableFocusActionBase<NextFocusIntent> {
Greg Spencer387e2b02019-06-04 11:30:24 -0700268 @override
Greg Spencer0f68b462020-04-07 16:49:39 -0700269 Memento invoke(NextFocusIntent intent) {
270 final Memento memento = super.invoke(intent);
271 primaryFocus.nextFocus();
272 return memento;
Greg Spencer387e2b02019-06-04 11:30:24 -0700273 }
274}
275
Greg Spencer0f68b462020-04-07 16:49:39 -0700276class UndoablePreviousFocusAction extends UndoableFocusActionBase<PreviousFocusIntent> {
Greg Spencer387e2b02019-06-04 11:30:24 -0700277 @override
Greg Spencer0f68b462020-04-07 16:49:39 -0700278 Memento invoke(PreviousFocusIntent intent) {
279 final Memento memento = super.invoke(intent);
280 primaryFocus.previousFocus();
281 return memento;
Greg Spencer387e2b02019-06-04 11:30:24 -0700282 }
283}
284
Greg Spencer0f68b462020-04-07 16:49:39 -0700285class UndoableDirectionalFocusAction extends UndoableFocusActionBase<DirectionalFocusIntent> {
Greg Spencer387e2b02019-06-04 11:30:24 -0700286 TraversalDirection direction;
287
288 @override
Greg Spencer0f68b462020-04-07 16:49:39 -0700289 Memento invoke(DirectionalFocusIntent intent) {
290 final Memento memento = super.invoke(intent);
291 primaryFocus.focusInDirection(intent.direction);
292 return memento;
Greg Spencer387e2b02019-06-04 11:30:24 -0700293 }
294}
295
296/// A button class that takes focus when clicked.
297class DemoButton extends StatefulWidget {
298 const DemoButton({this.name});
299
300 final String name;
301
302 @override
303 _DemoButtonState createState() => _DemoButtonState();
304}
305
306class _DemoButtonState extends State<DemoButton> {
307 FocusNode _focusNode;
Greg Spencer0f68b462020-04-07 16:49:39 -0700308 final GlobalKey _nameKey = GlobalKey();
Greg Spencer387e2b02019-06-04 11:30:24 -0700309
310 @override
311 void initState() {
312 super.initState();
313 _focusNode = FocusNode(debugLabel: widget.name);
314 }
315
316 void _handleOnPressed() {
317 print('Button ${widget.name} pressed.');
318 setState(() {
Greg Spencer0f68b462020-04-07 16:49:39 -0700319 Actions.invoke(_nameKey.currentContext, RequestFocusIntent(_focusNode));
Greg Spencer387e2b02019-06-04 11:30:24 -0700320 });
321 }
322
323 @override
324 void dispose() {
325 super.dispose();
326 _focusNode.dispose();
327 }
328
329 @override
330 Widget build(BuildContext context) {
331 return FlatButton(
332 focusNode: _focusNode,
333 focusColor: Colors.red,
334 hoverColor: Colors.blue,
335 onPressed: () => _handleOnPressed(),
Greg Spencer0f68b462020-04-07 16:49:39 -0700336 child: Text(widget.name, key: _nameKey),
Greg Spencer387e2b02019-06-04 11:30:24 -0700337 );
338 }
339}
340
341class FocusDemo extends StatefulWidget {
342 const FocusDemo({Key key}) : super(key: key);
343
Greg Spencer0f68b462020-04-07 16:49:39 -0700344 static GlobalKey appKey = GlobalKey();
345
Greg Spencer387e2b02019-06-04 11:30:24 -0700346 @override
347 _FocusDemoState createState() => _FocusDemoState();
348}
349
350class _FocusDemoState extends State<FocusDemo> {
351 FocusNode outlineFocus;
352 UndoableActionDispatcher dispatcher;
353 bool canUndo;
354 bool canRedo;
355
356 @override
357 void initState() {
358 super.initState();
359 outlineFocus = FocusNode(debugLabel: 'Demo Focus Node');
360 dispatcher = UndoableActionDispatcher();
361 canUndo = dispatcher.canUndo;
362 canRedo = dispatcher.canRedo;
363 dispatcher.addListener(_handleUndoStateChange);
364 }
365
366 void _handleUndoStateChange() {
367 if (dispatcher.canUndo != canUndo) {
368 setState(() {
369 canUndo = dispatcher.canUndo;
370 });
371 }
372 if (dispatcher.canRedo != canRedo) {
373 setState(() {
374 canRedo = dispatcher.canRedo;
375 });
376 }
377 }
378
379 @override
380 void dispose() {
381 dispatcher.removeListener(_handleUndoStateChange);
382 outlineFocus.dispose();
383 super.dispose();
384 }
385
386 @override
387 Widget build(BuildContext context) {
388 final TextTheme textTheme = Theme.of(context).textTheme;
Greg Spencerce150972019-10-10 13:49:33 -0700389 return Actions(
390 dispatcher: dispatcher,
Greg Spencer0f68b462020-04-07 16:49:39 -0700391 actions: <Type, Action<Intent>>{
392 RequestFocusIntent: UndoableRequestFocusAction(),
393 NextFocusIntent: UndoableNextFocusAction(),
394 PreviousFocusIntent: UndoablePreviousFocusAction(),
395 DirectionalFocusIntent: UndoableDirectionalFocusAction(),
396 UndoIntent: UndoAction(),
397 RedoIntent: RedoAction(),
Greg Spencer387e2b02019-06-04 11:30:24 -0700398 },
Greg Spencerd57d4932020-02-12 16:22:01 -0800399 child: FocusTraversalGroup(
Greg Spencerce150972019-10-10 13:49:33 -0700400 policy: ReadingOrderTraversalPolicy(),
401 child: Shortcuts(
402 shortcuts: <LogicalKeySet, Intent>{
Greg Spencer0f68b462020-04-07 16:49:39 -0700403 LogicalKeySet(Platform.isMacOS ? LogicalKeyboardKey.meta : LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.keyZ): const RedoIntent(),
404 LogicalKeySet(Platform.isMacOS ? LogicalKeyboardKey.meta : LogicalKeyboardKey.control, LogicalKeyboardKey.keyZ): const UndoIntent(),
Greg Spencerce150972019-10-10 13:49:33 -0700405 },
406 child: FocusScope(
Greg Spencer0f68b462020-04-07 16:49:39 -0700407 key: FocusDemo.appKey,
Greg Spencerce150972019-10-10 13:49:33 -0700408 debugLabel: 'Scope',
409 autofocus: true,
410 child: DefaultTextStyle(
Hans Mullerbc5c4642020-01-24 19:03:01 -0800411 style: textTheme.headline4,
Greg Spencerce150972019-10-10 13:49:33 -0700412 child: Scaffold(
413 appBar: AppBar(
414 title: const Text('Actions Demo'),
415 ),
416 body: Center(
417 child: Builder(builder: (BuildContext context) {
418 return Column(
419 mainAxisAlignment: MainAxisAlignment.center,
420 children: <Widget>[
421 Row(
422 mainAxisAlignment: MainAxisAlignment.center,
423 children: const <Widget>[
424 DemoButton(name: 'One'),
425 DemoButton(name: 'Two'),
426 DemoButton(name: 'Three'),
427 ],
428 ),
429 Row(
430 mainAxisAlignment: MainAxisAlignment.center,
431 children: const <Widget>[
432 DemoButton(name: 'Four'),
433 DemoButton(name: 'Five'),
434 DemoButton(name: 'Six'),
435 ],
436 ),
437 Row(
438 mainAxisAlignment: MainAxisAlignment.center,
439 children: const <Widget>[
440 DemoButton(name: 'Seven'),
441 DemoButton(name: 'Eight'),
442 DemoButton(name: 'Nine'),
443 ],
444 ),
445 Row(
446 mainAxisAlignment: MainAxisAlignment.center,
447 children: <Widget>[
448 Padding(
449 padding: const EdgeInsets.all(8.0),
450 child: RaisedButton(
451 child: const Text('UNDO'),
452 onPressed: canUndo
453 ? () {
Greg Spencer0f68b462020-04-07 16:49:39 -0700454 Actions.invoke(context, const UndoIntent());
Greg Spencerce150972019-10-10 13:49:33 -0700455 }
456 : null,
Greg Spencer387e2b02019-06-04 11:30:24 -0700457 ),
Greg Spencerce150972019-10-10 13:49:33 -0700458 ),
459 Padding(
460 padding: const EdgeInsets.all(8.0),
461 child: RaisedButton(
462 child: const Text('REDO'),
463 onPressed: canRedo
464 ? () {
Greg Spencer0f68b462020-04-07 16:49:39 -0700465 Actions.invoke(context, const RedoIntent());
Greg Spencerce150972019-10-10 13:49:33 -0700466 }
467 : null,
Greg Spencer387e2b02019-06-04 11:30:24 -0700468 ),
Greg Spencerce150972019-10-10 13:49:33 -0700469 ),
470 ],
471 ),
472 ],
473 );
474 }),
Greg Spencer387e2b02019-06-04 11:30:24 -0700475 ),
476 ),
477 ),
478 ),
479 ),
480 ),
481 );
482 }
483}