Ian Hickson | 449f4a6 | 2019-11-27 15:04:02 -0800 | [diff] [blame] | 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
Ian Hickson | 449f4a6 | 2019-11-27 15:04:02 -0800 | [diff] [blame] | 4 | |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 5 | import 'dart:collection'; |
| 6 | import 'dart:io'; |
| 7 | |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 8 | import 'package:flutter/foundation.dart'; |
| 9 | import 'package:flutter/material.dart'; |
| 10 | import 'package:flutter/services.dart'; |
| 11 | import 'package:flutter/widgets.dart'; |
| 12 | |
Greg Spencer | 245d1b5 | 2019-11-26 18:32:34 -0800 | [diff] [blame] | 13 | void main() { |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 14 | runApp(const MaterialApp( |
| 15 | title: 'Actions Demo', |
| 16 | home: FocusDemo(), |
| 17 | )); |
| 18 | } |
| 19 | |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 20 | /// 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 Spencer | 2163731 | 2020-06-16 09:25:04 -0700 | [diff] [blame] | 25 | class Memento extends Object with Diagnosticable { |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 26 | 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 Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 57 | /// Undoable Actions |
| 58 | |
| 59 | /// An [ActionDispatcher] subclass that manages the invocation of undoable |
| 60 | /// actions. |
| 61 | class 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 Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 72 | final DoubleLinkedQueue<Memento> _completedActions = DoubleLinkedQueue<Memento>(); |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 73 | // A stack of actions that can be redone. The most recent action performed is |
| 74 | // at the end of the list. |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 75 | final List<Memento> _undoneActions = <Memento>[]; |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 76 | |
| 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 Ardhuin | 4f9b6cf | 2020-01-07 16:32:04 +0100 | [diff] [blame] | 108 | for (final VoidCallback callback in _listeners) { |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 109 | callback(); |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | @override |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 114 | Object invokeAction(Action<Intent> action, Intent intent, [BuildContext context]) { |
| 115 | final Object result = super.invokeAction(action, intent, context); |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 116 | print('Invoking ${action is UndoableAction ? 'undoable ' : ''}$intent as $action: $this '); |
| 117 | if (action is UndoableAction) { |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 118 | _completedActions.addLast(result as Memento); |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 119 | _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 Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 129 | _completedActions.removeFirst(); |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 130 | } |
| 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 Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 136 | return _completedActions.first.canUndo; |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 137 | } |
| 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 Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 144 | return _undoneActions.first.canRedo; |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 145 | } |
| 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 Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 157 | final Memento memento = _completedActions.removeLast(); |
| 158 | memento.undo(); |
| 159 | _undoneActions.add(memento); |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 160 | 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 Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 172 | final Memento memento = _undoneActions.removeLast(); |
| 173 | final Memento replacement = memento.redo(); |
| 174 | _completedActions.add(replacement); |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 175 | _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 Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 185 | properties.add(IterableProperty<Memento>('undo stack', _completedActions)); |
| 186 | properties.add(IterableProperty<Memento>('redo stack', _undoneActions)); |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 187 | } |
| 188 | } |
| 189 | |
| 190 | class UndoIntent extends Intent { |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 191 | const UndoIntent(); |
| 192 | } |
| 193 | |
| 194 | class UndoAction extends Action<UndoIntent> { |
| 195 | @override |
Greg Spencer | 36767d0 | 2020-04-21 13:18:04 -0700 | [diff] [blame] | 196 | bool isEnabled(UndoIntent intent) { |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 197 | final UndoableActionDispatcher manager = Actions.of(primaryFocus?.context ?? FocusDemo.appKey.currentContext, nullOk: true) as UndoableActionDispatcher; |
| 198 | return manager.canUndo; |
| 199 | } |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 200 | |
| 201 | @override |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 202 | void invoke(UndoIntent intent) { |
| 203 | final UndoableActionDispatcher manager = Actions.of(primaryFocus?.context ?? FocusDemo.appKey.currentContext, nullOk: true) as UndoableActionDispatcher; |
| 204 | manager?.undo(); |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 205 | } |
| 206 | } |
| 207 | |
| 208 | class RedoIntent extends Intent { |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 209 | const RedoIntent(); |
| 210 | } |
| 211 | |
| 212 | class RedoAction extends Action<RedoIntent> { |
| 213 | @override |
Greg Spencer | 36767d0 | 2020-04-21 13:18:04 -0700 | [diff] [blame] | 214 | bool isEnabled(RedoIntent intent) { |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 215 | final UndoableActionDispatcher manager = Actions.of(primaryFocus.context, nullOk: true) as UndoableActionDispatcher; |
| 216 | return manager.canRedo; |
| 217 | } |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 218 | |
| 219 | @override |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 220 | RedoAction invoke(RedoIntent intent) { |
| 221 | final UndoableActionDispatcher manager = Actions.of(primaryFocus.context, nullOk: true) as UndoableActionDispatcher; |
| 222 | manager?.redo(); |
| 223 | return this; |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 224 | } |
| 225 | } |
| 226 | |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 227 | /// An action that can be undone. |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 228 | abstract class UndoableAction<T extends Intent> extends Action<T> { |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 229 | /// 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 Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 236 | @override |
| 237 | @mustCallSuper |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 238 | void invoke(T intent) { |
Greg Spencer | 0223565 | 2019-09-28 08:55:47 -0700 | [diff] [blame] | 239 | invocationIntent = intent; |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 240 | } |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 241 | } |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 242 | |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 243 | class UndoableFocusActionBase<T extends Intent> extends UndoableAction<T> { |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 244 | @override |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 245 | @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 Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 254 | } |
| 255 | } |
| 256 | |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 257 | class UndoableRequestFocusAction extends UndoableFocusActionBase<RequestFocusIntent> { |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 258 | @override |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 259 | Memento invoke(RequestFocusIntent intent) { |
| 260 | final Memento memento = super.invoke(intent); |
| 261 | intent.focusNode.requestFocus(); |
| 262 | return memento; |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 263 | } |
| 264 | } |
| 265 | |
| 266 | /// Actions for manipulating focus. |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 267 | class UndoableNextFocusAction extends UndoableFocusActionBase<NextFocusIntent> { |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 268 | @override |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 269 | Memento invoke(NextFocusIntent intent) { |
| 270 | final Memento memento = super.invoke(intent); |
| 271 | primaryFocus.nextFocus(); |
| 272 | return memento; |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 273 | } |
| 274 | } |
| 275 | |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 276 | class UndoablePreviousFocusAction extends UndoableFocusActionBase<PreviousFocusIntent> { |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 277 | @override |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 278 | Memento invoke(PreviousFocusIntent intent) { |
| 279 | final Memento memento = super.invoke(intent); |
| 280 | primaryFocus.previousFocus(); |
| 281 | return memento; |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 282 | } |
| 283 | } |
| 284 | |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 285 | class UndoableDirectionalFocusAction extends UndoableFocusActionBase<DirectionalFocusIntent> { |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 286 | TraversalDirection direction; |
| 287 | |
| 288 | @override |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 289 | Memento invoke(DirectionalFocusIntent intent) { |
| 290 | final Memento memento = super.invoke(intent); |
| 291 | primaryFocus.focusInDirection(intent.direction); |
| 292 | return memento; |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 293 | } |
| 294 | } |
| 295 | |
| 296 | /// A button class that takes focus when clicked. |
| 297 | class DemoButton extends StatefulWidget { |
| 298 | const DemoButton({this.name}); |
| 299 | |
| 300 | final String name; |
| 301 | |
| 302 | @override |
| 303 | _DemoButtonState createState() => _DemoButtonState(); |
| 304 | } |
| 305 | |
| 306 | class _DemoButtonState extends State<DemoButton> { |
| 307 | FocusNode _focusNode; |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 308 | final GlobalKey _nameKey = GlobalKey(); |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 309 | |
| 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 Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 319 | Actions.invoke(_nameKey.currentContext, RequestFocusIntent(_focusNode)); |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 320 | }); |
| 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 Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 336 | child: Text(widget.name, key: _nameKey), |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 337 | ); |
| 338 | } |
| 339 | } |
| 340 | |
| 341 | class FocusDemo extends StatefulWidget { |
| 342 | const FocusDemo({Key key}) : super(key: key); |
| 343 | |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 344 | static GlobalKey appKey = GlobalKey(); |
| 345 | |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 346 | @override |
| 347 | _FocusDemoState createState() => _FocusDemoState(); |
| 348 | } |
| 349 | |
| 350 | class _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 Spencer | ce15097 | 2019-10-10 13:49:33 -0700 | [diff] [blame] | 389 | return Actions( |
| 390 | dispatcher: dispatcher, |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 391 | actions: <Type, Action<Intent>>{ |
| 392 | RequestFocusIntent: UndoableRequestFocusAction(), |
| 393 | NextFocusIntent: UndoableNextFocusAction(), |
| 394 | PreviousFocusIntent: UndoablePreviousFocusAction(), |
| 395 | DirectionalFocusIntent: UndoableDirectionalFocusAction(), |
| 396 | UndoIntent: UndoAction(), |
| 397 | RedoIntent: RedoAction(), |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 398 | }, |
Greg Spencer | d57d493 | 2020-02-12 16:22:01 -0800 | [diff] [blame] | 399 | child: FocusTraversalGroup( |
Greg Spencer | ce15097 | 2019-10-10 13:49:33 -0700 | [diff] [blame] | 400 | policy: ReadingOrderTraversalPolicy(), |
| 401 | child: Shortcuts( |
| 402 | shortcuts: <LogicalKeySet, Intent>{ |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 403 | 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 Spencer | ce15097 | 2019-10-10 13:49:33 -0700 | [diff] [blame] | 405 | }, |
| 406 | child: FocusScope( |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 407 | key: FocusDemo.appKey, |
Greg Spencer | ce15097 | 2019-10-10 13:49:33 -0700 | [diff] [blame] | 408 | debugLabel: 'Scope', |
| 409 | autofocus: true, |
| 410 | child: DefaultTextStyle( |
Hans Muller | bc5c464 | 2020-01-24 19:03:01 -0800 | [diff] [blame] | 411 | style: textTheme.headline4, |
Greg Spencer | ce15097 | 2019-10-10 13:49:33 -0700 | [diff] [blame] | 412 | 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 Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 454 | Actions.invoke(context, const UndoIntent()); |
Greg Spencer | ce15097 | 2019-10-10 13:49:33 -0700 | [diff] [blame] | 455 | } |
| 456 | : null, |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 457 | ), |
Greg Spencer | ce15097 | 2019-10-10 13:49:33 -0700 | [diff] [blame] | 458 | ), |
| 459 | Padding( |
| 460 | padding: const EdgeInsets.all(8.0), |
| 461 | child: RaisedButton( |
| 462 | child: const Text('REDO'), |
| 463 | onPressed: canRedo |
| 464 | ? () { |
Greg Spencer | 0f68b46 | 2020-04-07 16:49:39 -0700 | [diff] [blame] | 465 | Actions.invoke(context, const RedoIntent()); |
Greg Spencer | ce15097 | 2019-10-10 13:49:33 -0700 | [diff] [blame] | 466 | } |
| 467 | : null, |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 468 | ), |
Greg Spencer | ce15097 | 2019-10-10 13:49:33 -0700 | [diff] [blame] | 469 | ), |
| 470 | ], |
| 471 | ), |
| 472 | ], |
| 473 | ); |
| 474 | }), |
Greg Spencer | 387e2b0 | 2019-06-04 11:30:24 -0700 | [diff] [blame] | 475 | ), |
| 476 | ), |
| 477 | ), |
| 478 | ), |
| 479 | ), |
| 480 | ), |
| 481 | ); |
| 482 | } |
| 483 | } |