"Fossies" - the Fresh Open Source Software Archive

Member "flutter-3.7.1/dev/manual_tests/lib/actions.dart" (1 Feb 2023, 15606 Bytes) of package /linux/misc/flutter-3.7.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Dart source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "actions.dart": 3.3.10_vs_3.7.0.

    1 // Copyright 2014 The Flutter Authors. All rights reserved.
    2 // Use of this source code is governed by a BSD-style license that can be
    3 // found in the LICENSE file.
    4 
    5 import 'dart:collection';
    6 import 'dart:io';
    7 
    8 import 'package:flutter/foundation.dart';
    9 import 'package:flutter/material.dart';
   10 import 'package:flutter/services.dart';
   11 
   12 void main() {
   13   runApp(const MaterialApp(
   14     title: 'Actions Demo',
   15     home: FocusDemo(),
   16   ));
   17 }
   18 
   19 /// A class that can hold invocation information that an [UndoableAction] can
   20 /// use to undo/redo itself.
   21 ///
   22 /// Instances of this class are returned from [UndoableAction]s and placed on
   23 /// the undo stack when they are invoked.
   24 class Memento extends Object with Diagnosticable {
   25   const Memento({
   26     required this.name,
   27     required this.undo,
   28     required this.redo,
   29   });
   30 
   31   /// Returns true if this Memento can be used to undo.
   32   ///
   33   /// Subclasses could override to provide their own conditions when a command is
   34   /// undoable.
   35   bool get canUndo => true;
   36 
   37   /// Returns true if this Memento can be used to redo.
   38   ///
   39   /// Subclasses could override to provide their own conditions when a command is
   40   /// redoable.
   41   bool get canRedo => true;
   42 
   43   final String name;
   44   final VoidCallback undo;
   45   final ValueGetter<Memento> redo;
   46 
   47   @override
   48   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
   49     super.debugFillProperties(properties);
   50     properties.add(StringProperty('name', name));
   51     properties.add(FlagProperty('undo', value: undo != null, ifTrue: 'undo'));
   52     properties.add(FlagProperty('redo', value: redo != null, ifTrue: 'redo'));
   53   }
   54 }
   55 
   56 /// Undoable Actions
   57 
   58 /// An [ActionDispatcher] subclass that manages the invocation of undoable
   59 /// actions.
   60 class UndoableActionDispatcher extends ActionDispatcher implements Listenable {
   61   /// Constructs a new [UndoableActionDispatcher].
   62   ///
   63   /// The [maxUndoLevels] argument must not be null.
   64   UndoableActionDispatcher({
   65     int maxUndoLevels = _defaultMaxUndoLevels,
   66   })  : assert(maxUndoLevels != null),
   67         _maxUndoLevels = maxUndoLevels;
   68 
   69   // A stack of actions that have been performed. The most recent action
   70   // performed is at the end of the list.
   71   final DoubleLinkedQueue<Memento> _completedActions = DoubleLinkedQueue<Memento>();
   72   // A stack of actions that can be redone. The most recent action performed is
   73   // at the end of the list.
   74   final List<Memento> _undoneActions = <Memento>[];
   75 
   76   static const int _defaultMaxUndoLevels = 1000;
   77 
   78   /// The maximum number of undo levels allowed.
   79   ///
   80   /// If this value is set to a value smaller than the number of completed
   81   /// actions, then the stack of completed actions is truncated to only include
   82   /// the last [maxUndoLevels] actions.
   83   int get maxUndoLevels => _maxUndoLevels;
   84   int _maxUndoLevels;
   85   set maxUndoLevels(int value) {
   86     _maxUndoLevels = value;
   87     _pruneActions();
   88   }
   89 
   90   final Set<VoidCallback> _listeners = <VoidCallback>{};
   91 
   92   @override
   93   void addListener(VoidCallback listener) {
   94     _listeners.add(listener);
   95   }
   96 
   97   @override
   98   void removeListener(VoidCallback listener) {
   99     _listeners.remove(listener);
  100   }
  101 
  102   /// Notifies listeners that the [ActionDispatcher] has changed state.
  103   ///
  104   /// May only be called by subclasses.
  105   @protected
  106   void notifyListeners() {
  107     for (final VoidCallback callback in _listeners) {
  108       callback();
  109     }
  110   }
  111 
  112   @override
  113   Object? invokeAction(Action<Intent> action, Intent intent, [BuildContext? context]) {
  114     final Object? result = super.invokeAction(action, intent, context);
  115     print('Invoking ${action is UndoableAction ? 'undoable ' : ''}$intent as $action: $this ');
  116     if (action is UndoableAction) {
  117       _completedActions.addLast(result! as Memento);
  118       _undoneActions.clear();
  119       _pruneActions();
  120       notifyListeners();
  121     }
  122     return result;
  123   }
  124 
  125   // Enforces undo level limit.
  126   void _pruneActions() {
  127     while (_completedActions.length > _maxUndoLevels) {
  128       _completedActions.removeFirst();
  129     }
  130   }
  131 
  132   /// Returns true if there is an action on the stack that can be undone.
  133   bool get canUndo {
  134     if (_completedActions.isNotEmpty) {
  135       return _completedActions.first.canUndo;
  136     }
  137     return false;
  138   }
  139 
  140   /// Returns true if an action that has been undone can be re-invoked.
  141   bool get canRedo {
  142     if (_undoneActions.isNotEmpty) {
  143       return _undoneActions.first.canRedo;
  144     }
  145     return false;
  146   }
  147 
  148   /// Undoes the last action executed if possible.
  149   ///
  150   /// Returns true if the action was successfully undone.
  151   bool undo() {
  152     print('Undoing. $this');
  153     if (!canUndo) {
  154       return false;
  155     }
  156     final Memento memento = _completedActions.removeLast();
  157     memento.undo();
  158     _undoneActions.add(memento);
  159     notifyListeners();
  160     return true;
  161   }
  162 
  163   /// Re-invokes a previously undone action, if possible.
  164   ///
  165   /// Returns true if the action was successfully invoked.
  166   bool redo() {
  167     print('Redoing. $this');
  168     if (!canRedo) {
  169       return false;
  170     }
  171     final Memento memento = _undoneActions.removeLast();
  172     final Memento replacement = memento.redo();
  173     _completedActions.add(replacement);
  174     _pruneActions();
  175     notifyListeners();
  176     return true;
  177   }
  178 
  179   @override
  180   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
  181     super.debugFillProperties(properties);
  182     properties.add(IntProperty('undoable items', _completedActions.length));
  183     properties.add(IntProperty('redoable items', _undoneActions.length));
  184     properties.add(IterableProperty<Memento>('undo stack', _completedActions));
  185     properties.add(IterableProperty<Memento>('redo stack', _undoneActions));
  186   }
  187 }
  188 
  189 class UndoIntent extends Intent {
  190   const UndoIntent();
  191 }
  192 
  193 class UndoAction extends Action<UndoIntent> {
  194   @override
  195   bool isEnabled(UndoIntent intent) {
  196     final BuildContext? buildContext = primaryFocus?.context ?? FocusDemo.appKey.currentContext;
  197     if (buildContext == null) {
  198       return false;
  199     }
  200     final UndoableActionDispatcher manager = Actions.of(buildContext) as UndoableActionDispatcher;
  201     return manager.canUndo;
  202   }
  203 
  204   @override
  205   void invoke(UndoIntent intent) {
  206     final BuildContext? buildContext = primaryFocus?.context ?? FocusDemo.appKey.currentContext;
  207     if (buildContext == null) {
  208       return;
  209     }
  210     final UndoableActionDispatcher manager = Actions.of(primaryFocus?.context ?? FocusDemo.appKey.currentContext!) as UndoableActionDispatcher;
  211     manager.undo();
  212   }
  213 }
  214 
  215 class RedoIntent extends Intent {
  216   const RedoIntent();
  217 }
  218 
  219 class RedoAction extends Action<RedoIntent> {
  220   @override
  221   bool isEnabled(RedoIntent intent) {
  222     final BuildContext? buildContext = primaryFocus?.context ?? FocusDemo.appKey.currentContext;
  223     if (buildContext == null) {
  224       return false;
  225     }
  226     final UndoableActionDispatcher manager = Actions.of(buildContext) as UndoableActionDispatcher;
  227     return manager.canRedo;
  228   }
  229 
  230   @override
  231   RedoAction invoke(RedoIntent intent) {
  232     final BuildContext? buildContext = primaryFocus?.context ?? FocusDemo.appKey.currentContext;
  233     if (buildContext == null) {
  234       return this;
  235     }
  236     final UndoableActionDispatcher manager = Actions.of(buildContext) as UndoableActionDispatcher;
  237     manager.redo();
  238     return this;
  239   }
  240 }
  241 
  242 /// An action that can be undone.
  243 abstract class UndoableAction<T extends Intent> extends Action<T> {
  244   /// The [Intent] this action was originally invoked with.
  245   Intent? get invocationIntent => _invocationTag;
  246   Intent? _invocationTag;
  247 
  248   @protected
  249   set invocationIntent(Intent? value) => _invocationTag = value;
  250 
  251   @override
  252   @mustCallSuper
  253   void invoke(T intent) {
  254     invocationIntent = intent;
  255   }
  256 }
  257 
  258 class UndoableFocusActionBase<T extends Intent> extends UndoableAction<T> {
  259   @override
  260   @mustCallSuper
  261   Memento invoke(T intent) {
  262     super.invoke(intent);
  263     final FocusNode? previousFocus = primaryFocus;
  264     return Memento(name: previousFocus!.debugLabel!, undo: () {
  265       previousFocus.requestFocus();
  266     }, redo: () {
  267       return invoke(intent);
  268     });
  269   }
  270 }
  271 
  272 class UndoableRequestFocusAction extends UndoableFocusActionBase<RequestFocusIntent> {
  273   @override
  274   Memento invoke(RequestFocusIntent intent) {
  275     final Memento memento = super.invoke(intent);
  276     intent.focusNode.requestFocus();
  277     return memento;
  278   }
  279 }
  280 
  281 /// Actions for manipulating focus.
  282 class UndoableNextFocusAction extends UndoableFocusActionBase<NextFocusIntent> {
  283   @override
  284   Memento invoke(NextFocusIntent intent) {
  285     final Memento memento = super.invoke(intent);
  286     primaryFocus?.nextFocus();
  287     return memento;
  288   }
  289 }
  290 
  291 class UndoablePreviousFocusAction extends UndoableFocusActionBase<PreviousFocusIntent> {
  292   @override
  293   Memento invoke(PreviousFocusIntent intent) {
  294     final Memento memento = super.invoke(intent);
  295     primaryFocus?.previousFocus();
  296     return memento;
  297   }
  298 }
  299 
  300 class UndoableDirectionalFocusAction extends UndoableFocusActionBase<DirectionalFocusIntent> {
  301   TraversalDirection? direction;
  302 
  303   @override
  304   Memento invoke(DirectionalFocusIntent intent) {
  305     final Memento memento = super.invoke(intent);
  306     primaryFocus?.focusInDirection(intent.direction);
  307     return memento;
  308   }
  309 }
  310 
  311 /// A button class that takes focus when clicked.
  312 class DemoButton extends StatefulWidget {
  313   const DemoButton({super.key, required this.name});
  314 
  315   final String name;
  316 
  317   @override
  318   State<DemoButton> createState() => _DemoButtonState();
  319 }
  320 
  321 class _DemoButtonState extends State<DemoButton> {
  322   late final FocusNode _focusNode = FocusNode(debugLabel: widget.name);
  323   final GlobalKey _nameKey = GlobalKey();
  324 
  325   void _handleOnPressed() {
  326     print('Button ${widget.name} pressed.');
  327     setState(() {
  328       Actions.invoke(_nameKey.currentContext!, RequestFocusIntent(_focusNode));
  329     });
  330   }
  331 
  332   @override
  333   void dispose() {
  334     super.dispose();
  335     _focusNode.dispose();
  336   }
  337 
  338   @override
  339   Widget build(BuildContext context) {
  340     return TextButton(
  341       focusNode: _focusNode,
  342       style: ButtonStyle(
  343         foregroundColor: const MaterialStatePropertyAll<Color>(Colors.black),
  344         overlayColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) {
  345           if (states.contains(MaterialState.focused)) {
  346             return Colors.red;
  347           }
  348           if (states.contains(MaterialState.hovered)) {
  349             return Colors.blue;
  350           }
  351           return Colors.transparent;
  352         }),
  353       ),
  354       onPressed: () => _handleOnPressed(),
  355       child: Text(widget.name, key: _nameKey),
  356     );
  357   }
  358 }
  359 
  360 class FocusDemo extends StatefulWidget {
  361   const FocusDemo({super.key});
  362 
  363   static GlobalKey appKey = GlobalKey();
  364 
  365   @override
  366   State<FocusDemo> createState() => _FocusDemoState();
  367 }
  368 
  369 class _FocusDemoState extends State<FocusDemo> {
  370   final FocusNode outlineFocus = FocusNode(debugLabel: 'Demo Focus Node');
  371   late final UndoableActionDispatcher dispatcher = UndoableActionDispatcher();
  372   bool canUndo = false;
  373   bool canRedo = false;
  374 
  375   @override
  376   void initState() {
  377     super.initState();
  378     canUndo = dispatcher.canUndo;
  379     canRedo = dispatcher.canRedo;
  380     dispatcher.addListener(_handleUndoStateChange);
  381   }
  382 
  383   void _handleUndoStateChange() {
  384     if (dispatcher.canUndo != canUndo) {
  385       setState(() {
  386         canUndo = dispatcher.canUndo;
  387       });
  388     }
  389     if (dispatcher.canRedo != canRedo) {
  390       setState(() {
  391         canRedo = dispatcher.canRedo;
  392       });
  393     }
  394   }
  395 
  396   @override
  397   void dispose() {
  398     dispatcher.removeListener(_handleUndoStateChange);
  399     outlineFocus.dispose();
  400     super.dispose();
  401   }
  402 
  403   @override
  404   Widget build(BuildContext context) {
  405     final TextTheme textTheme = Theme.of(context).textTheme;
  406     return Actions(
  407       dispatcher: dispatcher,
  408       actions: <Type, Action<Intent>>{
  409         RequestFocusIntent: UndoableRequestFocusAction(),
  410         NextFocusIntent: UndoableNextFocusAction(),
  411         PreviousFocusIntent: UndoablePreviousFocusAction(),
  412         DirectionalFocusIntent: UndoableDirectionalFocusAction(),
  413         UndoIntent: UndoAction(),
  414         RedoIntent: RedoAction(),
  415       },
  416       child: FocusTraversalGroup(
  417         policy: ReadingOrderTraversalPolicy(),
  418         child: Shortcuts(
  419           shortcuts: <ShortcutActivator, Intent>{
  420             SingleActivator(LogicalKeyboardKey.keyZ, meta: Platform.isMacOS, control: !Platform.isMacOS, shift: true): const RedoIntent(),
  421             SingleActivator(LogicalKeyboardKey.keyZ, meta: Platform.isMacOS, control: !Platform.isMacOS): const UndoIntent(),
  422           },
  423           child: FocusScope(
  424             key: FocusDemo.appKey,
  425             debugLabel: 'Scope',
  426             autofocus: true,
  427             child: DefaultTextStyle(
  428               style: textTheme.headlineMedium!,
  429               child: Scaffold(
  430                 appBar: AppBar(
  431                   title: const Text('Actions Demo'),
  432                 ),
  433                 body: Center(
  434                   child: Builder(builder: (BuildContext context) {
  435                     return Column(
  436                       mainAxisAlignment: MainAxisAlignment.center,
  437                       children: <Widget>[
  438                         Row(
  439                           mainAxisAlignment: MainAxisAlignment.center,
  440                           children: const <Widget>[
  441                             DemoButton(name: 'One'),
  442                             DemoButton(name: 'Two'),
  443                             DemoButton(name: 'Three'),
  444                           ],
  445                         ),
  446                         Row(
  447                           mainAxisAlignment: MainAxisAlignment.center,
  448                           children: const <Widget>[
  449                             DemoButton(name: 'Four'),
  450                             DemoButton(name: 'Five'),
  451                             DemoButton(name: 'Six'),
  452                           ],
  453                         ),
  454                         Row(
  455                           mainAxisAlignment: MainAxisAlignment.center,
  456                           children: const <Widget>[
  457                             DemoButton(name: 'Seven'),
  458                             DemoButton(name: 'Eight'),
  459                             DemoButton(name: 'Nine'),
  460                           ],
  461                         ),
  462                         Row(
  463                           mainAxisAlignment: MainAxisAlignment.center,
  464                           children: <Widget>[
  465                             Padding(
  466                               padding: const EdgeInsets.all(8.0),
  467                               child: ElevatedButton(
  468                                 onPressed: canUndo
  469                                     ? () {
  470                                         Actions.invoke(context, const UndoIntent());
  471                                       }
  472                                     : null,
  473                                 child: const Text('UNDO'),
  474                               ),
  475                             ),
  476                             Padding(
  477                               padding: const EdgeInsets.all(8.0),
  478                               child: ElevatedButton(
  479                                 onPressed: canRedo
  480                                     ? () {
  481                                         Actions.invoke(context, const RedoIntent());
  482                                       }
  483                                     : null,
  484                                 child: const Text('REDO'),
  485                               ),
  486                             ),
  487                           ],
  488                         ),
  489                       ],
  490                     );
  491                   }),
  492                 ),
  493               ),
  494             ),
  495           ),
  496         ),
  497       ),
  498     );
  499   }
  500 }