| // Copyright 2014 The Flutter Authors. All rights reserved. | 
 | // Use of this source code is governed by a BSD-style license that can be | 
 | // found in the LICENSE file. | 
 |  | 
 | import 'dart:collection'; | 
 | import 'dart:io'; | 
 |  | 
 | import 'package:flutter/foundation.dart'; | 
 | import 'package:flutter/material.dart'; | 
 | import 'package:flutter/services.dart'; | 
 |  | 
 | void main() { | 
 |   runApp(const MaterialApp( | 
 |     title: 'Actions Demo', | 
 |     home: FocusDemo(), | 
 |   )); | 
 | } | 
 |  | 
 | /// A class that can hold invocation information that an [UndoableAction] can | 
 | /// use to undo/redo itself. | 
 | /// | 
 | /// Instances of this class are returned from [UndoableAction]s and placed on | 
 | /// the undo stack when they are invoked. | 
 | class Memento extends Object with Diagnosticable { | 
 |   const Memento({ | 
 |     required this.name, | 
 |     required this.undo, | 
 |     required this.redo, | 
 |   }); | 
 |  | 
 |   /// Returns true if this Memento can be used to undo. | 
 |   /// | 
 |   /// Subclasses could override to provide their own conditions when a command is | 
 |   /// undoable. | 
 |   bool get canUndo => true; | 
 |  | 
 |   /// Returns true if this Memento can be used to redo. | 
 |   /// | 
 |   /// Subclasses could override to provide their own conditions when a command is | 
 |   /// redoable. | 
 |   bool get canRedo => true; | 
 |  | 
 |   final String name; | 
 |   final VoidCallback undo; | 
 |   final ValueGetter<Memento> redo; | 
 |  | 
 |   @override | 
 |   void debugFillProperties(DiagnosticPropertiesBuilder properties) { | 
 |     super.debugFillProperties(properties); | 
 |     properties.add(StringProperty('name', name)); | 
 |     properties.add(FlagProperty('undo', value: undo != null, ifTrue: 'undo')); | 
 |     properties.add(FlagProperty('redo', value: redo != null, ifTrue: 'redo')); | 
 |   } | 
 | } | 
 |  | 
 | /// Undoable Actions | 
 |  | 
 | /// An [ActionDispatcher] subclass that manages the invocation of undoable | 
 | /// actions. | 
 | class UndoableActionDispatcher extends ActionDispatcher implements Listenable { | 
 |   /// Constructs a new [UndoableActionDispatcher]. | 
 |   /// | 
 |   /// The [maxUndoLevels] argument must not be null. | 
 |   UndoableActionDispatcher({ | 
 |     int maxUndoLevels = _defaultMaxUndoLevels, | 
 |   })  : assert(maxUndoLevels != null), | 
 |         _maxUndoLevels = maxUndoLevels; | 
 |  | 
 |   // A stack of actions that have been performed. The most recent action | 
 |   // performed is at the end of the list. | 
 |   final DoubleLinkedQueue<Memento> _completedActions = DoubleLinkedQueue<Memento>(); | 
 |   // A stack of actions that can be redone. The most recent action performed is | 
 |   // at the end of the list. | 
 |   final List<Memento> _undoneActions = <Memento>[]; | 
 |  | 
 |   static const int _defaultMaxUndoLevels = 1000; | 
 |  | 
 |   /// The maximum number of undo levels allowed. | 
 |   /// | 
 |   /// If this value is set to a value smaller than the number of completed | 
 |   /// actions, then the stack of completed actions is truncated to only include | 
 |   /// the last [maxUndoLevels] actions. | 
 |   int get maxUndoLevels => _maxUndoLevels; | 
 |   int _maxUndoLevels; | 
 |   set maxUndoLevels(int value) { | 
 |     _maxUndoLevels = value; | 
 |     _pruneActions(); | 
 |   } | 
 |  | 
 |   final Set<VoidCallback> _listeners = <VoidCallback>{}; | 
 |  | 
 |   @override | 
 |   void addListener(VoidCallback listener) { | 
 |     _listeners.add(listener); | 
 |   } | 
 |  | 
 |   @override | 
 |   void removeListener(VoidCallback listener) { | 
 |     _listeners.remove(listener); | 
 |   } | 
 |  | 
 |   /// Notifies listeners that the [ActionDispatcher] has changed state. | 
 |   /// | 
 |   /// May only be called by subclasses. | 
 |   @protected | 
 |   void notifyListeners() { | 
 |     for (final VoidCallback callback in _listeners) { | 
 |       callback(); | 
 |     } | 
 |   } | 
 |  | 
 |   @override | 
 |   Object? invokeAction(Action<Intent> action, Intent intent, [BuildContext? context]) { | 
 |     final Object? result = super.invokeAction(action, intent, context); | 
 |     print('Invoking ${action is UndoableAction ? 'undoable ' : ''}$intent as $action: $this '); | 
 |     if (action is UndoableAction) { | 
 |       _completedActions.addLast(result! as Memento); | 
 |       _undoneActions.clear(); | 
 |       _pruneActions(); | 
 |       notifyListeners(); | 
 |     } | 
 |     return result; | 
 |   } | 
 |  | 
 |   // Enforces undo level limit. | 
 |   void _pruneActions() { | 
 |     while (_completedActions.length > _maxUndoLevels) { | 
 |       _completedActions.removeFirst(); | 
 |     } | 
 |   } | 
 |  | 
 |   /// Returns true if there is an action on the stack that can be undone. | 
 |   bool get canUndo { | 
 |     if (_completedActions.isNotEmpty) { | 
 |       return _completedActions.first.canUndo; | 
 |     } | 
 |     return false; | 
 |   } | 
 |  | 
 |   /// Returns true if an action that has been undone can be re-invoked. | 
 |   bool get canRedo { | 
 |     if (_undoneActions.isNotEmpty) { | 
 |       return _undoneActions.first.canRedo; | 
 |     } | 
 |     return false; | 
 |   } | 
 |  | 
 |   /// Undoes the last action executed if possible. | 
 |   /// | 
 |   /// Returns true if the action was successfully undone. | 
 |   bool undo() { | 
 |     print('Undoing. $this'); | 
 |     if (!canUndo) { | 
 |       return false; | 
 |     } | 
 |     final Memento memento = _completedActions.removeLast(); | 
 |     memento.undo(); | 
 |     _undoneActions.add(memento); | 
 |     notifyListeners(); | 
 |     return true; | 
 |   } | 
 |  | 
 |   /// Re-invokes a previously undone action, if possible. | 
 |   /// | 
 |   /// Returns true if the action was successfully invoked. | 
 |   bool redo() { | 
 |     print('Redoing. $this'); | 
 |     if (!canRedo) { | 
 |       return false; | 
 |     } | 
 |     final Memento memento = _undoneActions.removeLast(); | 
 |     final Memento replacement = memento.redo(); | 
 |     _completedActions.add(replacement); | 
 |     _pruneActions(); | 
 |     notifyListeners(); | 
 |     return true; | 
 |   } | 
 |  | 
 |   @override | 
 |   void debugFillProperties(DiagnosticPropertiesBuilder properties) { | 
 |     super.debugFillProperties(properties); | 
 |     properties.add(IntProperty('undoable items', _completedActions.length)); | 
 |     properties.add(IntProperty('redoable items', _undoneActions.length)); | 
 |     properties.add(IterableProperty<Memento>('undo stack', _completedActions)); | 
 |     properties.add(IterableProperty<Memento>('redo stack', _undoneActions)); | 
 |   } | 
 | } | 
 |  | 
 | class UndoIntent extends Intent { | 
 |   const UndoIntent(); | 
 | } | 
 |  | 
 | class UndoAction extends Action<UndoIntent> { | 
 |   @override | 
 |   bool isEnabled(UndoIntent intent) { | 
 |     final BuildContext? buildContext = primaryFocus?.context ?? FocusDemo.appKey.currentContext; | 
 |     if (buildContext == null) { | 
 |       return false; | 
 |     } | 
 |     final UndoableActionDispatcher manager = Actions.of(buildContext) as UndoableActionDispatcher; | 
 |     return manager.canUndo; | 
 |   } | 
 |  | 
 |   @override | 
 |   void invoke(UndoIntent intent) { | 
 |     final BuildContext? buildContext = primaryFocus?.context ?? FocusDemo.appKey.currentContext; | 
 |     if (buildContext == null) { | 
 |       return; | 
 |     } | 
 |     final UndoableActionDispatcher manager = Actions.of(primaryFocus?.context ?? FocusDemo.appKey.currentContext!) as UndoableActionDispatcher; | 
 |     manager.undo(); | 
 |   } | 
 | } | 
 |  | 
 | class RedoIntent extends Intent { | 
 |   const RedoIntent(); | 
 | } | 
 |  | 
 | class RedoAction extends Action<RedoIntent> { | 
 |   @override | 
 |   bool isEnabled(RedoIntent intent) { | 
 |     final BuildContext? buildContext = primaryFocus?.context ?? FocusDemo.appKey.currentContext; | 
 |     if (buildContext == null) { | 
 |       return false; | 
 |     } | 
 |     final UndoableActionDispatcher manager = Actions.of(buildContext) as UndoableActionDispatcher; | 
 |     return manager.canRedo; | 
 |   } | 
 |  | 
 |   @override | 
 |   RedoAction invoke(RedoIntent intent) { | 
 |     final BuildContext? buildContext = primaryFocus?.context ?? FocusDemo.appKey.currentContext; | 
 |     if (buildContext == null) { | 
 |       return this; | 
 |     } | 
 |     final UndoableActionDispatcher manager = Actions.of(buildContext) as UndoableActionDispatcher; | 
 |     manager.redo(); | 
 |     return this; | 
 |   } | 
 | } | 
 |  | 
 | /// An action that can be undone. | 
 | abstract class UndoableAction<T extends Intent> extends Action<T> { | 
 |   /// The [Intent] this action was originally invoked with. | 
 |   Intent? get invocationIntent => _invocationTag; | 
 |   Intent? _invocationTag; | 
 |  | 
 |   @protected | 
 |   set invocationIntent(Intent? value) => _invocationTag = value; | 
 |  | 
 |   @override | 
 |   @mustCallSuper | 
 |   void invoke(T intent) { | 
 |     invocationIntent = intent; | 
 |   } | 
 | } | 
 |  | 
 | class UndoableFocusActionBase<T extends Intent> extends UndoableAction<T> { | 
 |   @override | 
 |   @mustCallSuper | 
 |   Memento invoke(T intent) { | 
 |     super.invoke(intent); | 
 |     final FocusNode? previousFocus = primaryFocus; | 
 |     return Memento(name: previousFocus!.debugLabel!, undo: () { | 
 |       previousFocus.requestFocus(); | 
 |     }, redo: () { | 
 |       return invoke(intent); | 
 |     }); | 
 |   } | 
 | } | 
 |  | 
 | class UndoableRequestFocusAction extends UndoableFocusActionBase<RequestFocusIntent> { | 
 |   @override | 
 |   Memento invoke(RequestFocusIntent intent) { | 
 |     final Memento memento = super.invoke(intent); | 
 |     intent.focusNode.requestFocus(); | 
 |     return memento; | 
 |   } | 
 | } | 
 |  | 
 | /// Actions for manipulating focus. | 
 | class UndoableNextFocusAction extends UndoableFocusActionBase<NextFocusIntent> { | 
 |   @override | 
 |   Memento invoke(NextFocusIntent intent) { | 
 |     final Memento memento = super.invoke(intent); | 
 |     primaryFocus?.nextFocus(); | 
 |     return memento; | 
 |   } | 
 | } | 
 |  | 
 | class UndoablePreviousFocusAction extends UndoableFocusActionBase<PreviousFocusIntent> { | 
 |   @override | 
 |   Memento invoke(PreviousFocusIntent intent) { | 
 |     final Memento memento = super.invoke(intent); | 
 |     primaryFocus?.previousFocus(); | 
 |     return memento; | 
 |   } | 
 | } | 
 |  | 
 | class UndoableDirectionalFocusAction extends UndoableFocusActionBase<DirectionalFocusIntent> { | 
 |   TraversalDirection? direction; | 
 |  | 
 |   @override | 
 |   Memento invoke(DirectionalFocusIntent intent) { | 
 |     final Memento memento = super.invoke(intent); | 
 |     primaryFocus?.focusInDirection(intent.direction); | 
 |     return memento; | 
 |   } | 
 | } | 
 |  | 
 | /// A button class that takes focus when clicked. | 
 | class DemoButton extends StatefulWidget { | 
 |   const DemoButton({Key? key, required this.name}) : super(key: key); | 
 |  | 
 |   final String name; | 
 |  | 
 |   @override | 
 |   State<DemoButton> createState() => _DemoButtonState(); | 
 | } | 
 |  | 
 | class _DemoButtonState extends State<DemoButton> { | 
 |   late final FocusNode _focusNode = FocusNode(debugLabel: widget.name); | 
 |   final GlobalKey _nameKey = GlobalKey(); | 
 |  | 
 |   void _handleOnPressed() { | 
 |     print('Button ${widget.name} pressed.'); | 
 |     setState(() { | 
 |       Actions.invoke(_nameKey.currentContext!, RequestFocusIntent(_focusNode)); | 
 |     }); | 
 |   } | 
 |  | 
 |   @override | 
 |   void dispose() { | 
 |     super.dispose(); | 
 |     _focusNode.dispose(); | 
 |   } | 
 |  | 
 |   @override | 
 |   Widget build(BuildContext context) { | 
 |     return TextButton( | 
 |       focusNode: _focusNode, | 
 |       style: ButtonStyle( | 
 |         foregroundColor: MaterialStateProperty.all<Color>(Colors.black), | 
 |         overlayColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) { | 
 |           if (states.contains(MaterialState.focused)) | 
 |             return Colors.red; | 
 |           if (states.contains(MaterialState.hovered)) | 
 |             return Colors.blue; | 
 |           return Colors.transparent; | 
 |         }), | 
 |       ), | 
 |       onPressed: () => _handleOnPressed(), | 
 |       child: Text(widget.name, key: _nameKey), | 
 |     ); | 
 |   } | 
 | } | 
 |  | 
 | class FocusDemo extends StatefulWidget { | 
 |   const FocusDemo({Key? key}) : super(key: key); | 
 |  | 
 |   static GlobalKey appKey = GlobalKey(); | 
 |  | 
 |   @override | 
 |   State<FocusDemo> createState() => _FocusDemoState(); | 
 | } | 
 |  | 
 | class _FocusDemoState extends State<FocusDemo> { | 
 |   final FocusNode outlineFocus = FocusNode(debugLabel: 'Demo Focus Node'); | 
 |   late final UndoableActionDispatcher dispatcher = UndoableActionDispatcher(); | 
 |   bool canUndo = false; | 
 |   bool canRedo = false; | 
 |  | 
 |   @override | 
 |   void initState() { | 
 |     super.initState(); | 
 |     canUndo = dispatcher.canUndo; | 
 |     canRedo = dispatcher.canRedo; | 
 |     dispatcher.addListener(_handleUndoStateChange); | 
 |   } | 
 |  | 
 |   void _handleUndoStateChange() { | 
 |     if (dispatcher.canUndo != canUndo) { | 
 |       setState(() { | 
 |         canUndo = dispatcher.canUndo; | 
 |       }); | 
 |     } | 
 |     if (dispatcher.canRedo != canRedo) { | 
 |       setState(() { | 
 |         canRedo = dispatcher.canRedo; | 
 |       }); | 
 |     } | 
 |   } | 
 |  | 
 |   @override | 
 |   void dispose() { | 
 |     dispatcher.removeListener(_handleUndoStateChange); | 
 |     outlineFocus.dispose(); | 
 |     super.dispose(); | 
 |   } | 
 |  | 
 |   @override | 
 |   Widget build(BuildContext context) { | 
 |     final TextTheme textTheme = Theme.of(context).textTheme; | 
 |     return Actions( | 
 |       dispatcher: dispatcher, | 
 |       actions: <Type, Action<Intent>>{ | 
 |         RequestFocusIntent: UndoableRequestFocusAction(), | 
 |         NextFocusIntent: UndoableNextFocusAction(), | 
 |         PreviousFocusIntent: UndoablePreviousFocusAction(), | 
 |         DirectionalFocusIntent: UndoableDirectionalFocusAction(), | 
 |         UndoIntent: UndoAction(), | 
 |         RedoIntent: RedoAction(), | 
 |       }, | 
 |       child: FocusTraversalGroup( | 
 |         policy: ReadingOrderTraversalPolicy(), | 
 |         child: Shortcuts( | 
 |           shortcuts: <ShortcutActivator, Intent>{ | 
 |             SingleActivator(LogicalKeyboardKey.keyZ, meta: Platform.isMacOS, control: !Platform.isMacOS, shift: true): const RedoIntent(), | 
 |             SingleActivator(LogicalKeyboardKey.keyZ, meta: Platform.isMacOS, control: !Platform.isMacOS): const UndoIntent(), | 
 |           }, | 
 |           child: FocusScope( | 
 |             key: FocusDemo.appKey, | 
 |             debugLabel: 'Scope', | 
 |             autofocus: true, | 
 |             child: DefaultTextStyle( | 
 |               style: textTheme.headline4!, | 
 |               child: Scaffold( | 
 |                 appBar: AppBar( | 
 |                   title: const Text('Actions Demo'), | 
 |                 ), | 
 |                 body: Center( | 
 |                   child: Builder(builder: (BuildContext context) { | 
 |                     return Column( | 
 |                       mainAxisAlignment: MainAxisAlignment.center, | 
 |                       children: <Widget>[ | 
 |                         Row( | 
 |                           mainAxisAlignment: MainAxisAlignment.center, | 
 |                           children: const <Widget>[ | 
 |                             DemoButton(name: 'One'), | 
 |                             DemoButton(name: 'Two'), | 
 |                             DemoButton(name: 'Three'), | 
 |                           ], | 
 |                         ), | 
 |                         Row( | 
 |                           mainAxisAlignment: MainAxisAlignment.center, | 
 |                           children: const <Widget>[ | 
 |                             DemoButton(name: 'Four'), | 
 |                             DemoButton(name: 'Five'), | 
 |                             DemoButton(name: 'Six'), | 
 |                           ], | 
 |                         ), | 
 |                         Row( | 
 |                           mainAxisAlignment: MainAxisAlignment.center, | 
 |                           children: const <Widget>[ | 
 |                             DemoButton(name: 'Seven'), | 
 |                             DemoButton(name: 'Eight'), | 
 |                             DemoButton(name: 'Nine'), | 
 |                           ], | 
 |                         ), | 
 |                         Row( | 
 |                           mainAxisAlignment: MainAxisAlignment.center, | 
 |                           children: <Widget>[ | 
 |                             Padding( | 
 |                               padding: const EdgeInsets.all(8.0), | 
 |                               child: ElevatedButton( | 
 |                                 onPressed: canUndo | 
 |                                     ? () { | 
 |                                         Actions.invoke(context, const UndoIntent()); | 
 |                                       } | 
 |                                     : null, | 
 |                                 child: const Text('UNDO'), | 
 |                               ), | 
 |                             ), | 
 |                             Padding( | 
 |                               padding: const EdgeInsets.all(8.0), | 
 |                               child: ElevatedButton( | 
 |                                 onPressed: canRedo | 
 |                                     ? () { | 
 |                                         Actions.invoke(context, const RedoIntent()); | 
 |                                       } | 
 |                                     : null, | 
 |                                 child: const Text('REDO'), | 
 |                               ), | 
 |                             ), | 
 |                           ], | 
 |                         ), | 
 |                       ], | 
 |                     ); | 
 |                   }), | 
 |                 ), | 
 |               ), | 
 |             ), | 
 |           ), | 
 |         ), | 
 |       ), | 
 |     ); | 
 |   } | 
 | } |