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 | /// @docImport 'package:flutter/material.dart'; |
6 | /// |
7 | /// @docImport 'editable_text.dart'; |
8 | /// @docImport 'focus_scope.dart'; |
9 | library; |
10 | |
11 | import 'dart:async'; |
12 | |
13 | import 'package:flutter/foundation.dart'; |
14 | import 'package:flutter/services.dart'; |
15 | |
16 | import 'actions.dart'; |
17 | import 'focus_manager.dart'; |
18 | import 'framework.dart'; |
19 | import 'text_editing_intents.dart'; |
20 | |
21 | /// Provides undo/redo capabilities for a [ValueNotifier]. |
22 | /// |
23 | /// Listens to [value] and saves relevant values for undoing/redoing. The |
24 | /// cadence at which values are saved is a best approximation of the native |
25 | /// behaviors of a number of hardware keyboard on Flutter's desktop |
26 | /// platforms, as there are subtle differences between each of the platforms. |
27 | /// |
28 | /// Listens to keyboard undo/redo shortcuts and calls [onTriggered] when a |
29 | /// shortcut is triggered that would affect the state of the [value]. |
30 | /// |
31 | /// The [child] must manage focus on the [focusNode]. For example, using a |
32 | /// [TextField] or [Focus] widget. |
33 | class UndoHistory<T> extends StatefulWidget { |
34 | /// Creates an instance of [UndoHistory]. |
35 | const UndoHistory({ |
36 | super.key, |
37 | this.shouldChangeUndoStack, |
38 | required this.value, |
39 | required this.onTriggered, |
40 | required this.focusNode, |
41 | this.undoStackModifier, |
42 | this.controller, |
43 | required this.child, |
44 | }); |
45 | |
46 | /// The value to track over time. |
47 | final ValueNotifier<T> value; |
48 | |
49 | /// Called when checking whether a value change should be pushed onto |
50 | /// the undo stack. |
51 | final bool Function(T? oldValue, T newValue)? shouldChangeUndoStack; |
52 | |
53 | /// Called right before a new entry is pushed to the undo stack. |
54 | /// |
55 | /// The value returned from this method will be pushed to the stack instead |
56 | /// of the original value. |
57 | /// |
58 | /// If null then the original value will always be pushed to the stack. |
59 | final T Function(T value)? undoStackModifier; |
60 | |
61 | /// Called when an undo or redo causes a state change. |
62 | /// |
63 | /// If the state would still be the same before and after the undo/redo, this |
64 | /// will not be called. For example, receiving a redo when there is nothing |
65 | /// to redo will not call this method. |
66 | /// |
67 | /// Changes to the [value] while this method is running will not be recorded |
68 | /// on the undo stack. For example, a [TextInputFormatter] may change the value |
69 | /// from what was on the undo stack, but this new value will not be recorded, |
70 | /// as that would wipe out the redo history. |
71 | final void Function(T value) onTriggered; |
72 | |
73 | /// The [FocusNode] that will be used to listen for focus to set the initial |
74 | /// undo state for the element. |
75 | final FocusNode focusNode; |
76 | |
77 | /// {@template flutter.widgets.undoHistory.controller} |
78 | /// Controls the undo state. |
79 | /// |
80 | /// If null, this widget will create its own [UndoHistoryController]. |
81 | /// {@endtemplate} |
82 | final UndoHistoryController? controller; |
83 | |
84 | /// The child widget of [UndoHistory]. |
85 | final Widget child; |
86 | |
87 | @override |
88 | State<UndoHistory<T>> createState() => UndoHistoryState<T>(); |
89 | } |
90 | |
91 | /// State for a [UndoHistory]. |
92 | /// |
93 | /// Provides [undo], [redo], [canUndo], and [canRedo] for programmatic access |
94 | /// to the undo state for custom undo and redo UI implementations. |
95 | @visibleForTesting |
96 | class UndoHistoryState<T> extends State<UndoHistory<T>> with UndoManagerClient { |
97 | final _UndoStack<T> _stack = _UndoStack<T>(); |
98 | late final _Throttled<T> _throttledPush; |
99 | Timer? _throttleTimer; |
100 | bool _duringTrigger = false; |
101 | |
102 | // This duration was chosen as a best fit for the behavior of Mac, Linux, |
103 | // and Windows undo/redo state save durations, but it is not perfect for any |
104 | // of them. |
105 | static const Duration _kThrottleDuration = Duration(milliseconds: 500); |
106 | |
107 | // Record the last value to prevent pushing multiple |
108 | // of the same value in a row onto the undo stack. For example, _push gets |
109 | // called both in initState and when the EditableText receives focus. |
110 | T? _lastValue; |
111 | |
112 | UndoHistoryController? _controller; |
113 | |
114 | UndoHistoryController get _effectiveController => widget.controller ?? (_controller ??= UndoHistoryController()); |
115 | |
116 | @override |
117 | void undo() { |
118 | if (_stack.currentValue == null) { |
119 | // Returns early if there is not a first value registered in the history. |
120 | // This is important because, if an undo is received while the initial |
121 | // value is being pushed (a.k.a when the field gets the focus but the |
122 | // throttling delay is pending), the initial push should not be canceled. |
123 | return; |
124 | } |
125 | if (_throttleTimer?.isActive ?? false) { |
126 | _throttleTimer?.cancel(); // Cancel ongoing push, if any. |
127 | _update(_stack.currentValue); |
128 | } else { |
129 | _update(_stack.undo()); |
130 | } |
131 | _updateState(); |
132 | } |
133 | |
134 | @override |
135 | void redo() { |
136 | _update(_stack.redo()); |
137 | _updateState(); |
138 | } |
139 | |
140 | @override |
141 | bool get canUndo => _stack.canUndo; |
142 | |
143 | @override |
144 | bool get canRedo => _stack.canRedo; |
145 | |
146 | void _updateState() { |
147 | _effectiveController.value = UndoHistoryValue(canUndo: canUndo, canRedo: canRedo); |
148 | |
149 | if (defaultTargetPlatform != TargetPlatform.iOS) { |
150 | return; |
151 | } |
152 | |
153 | if (UndoManager.client == this) { |
154 | UndoManager.setUndoState(canUndo: canUndo, canRedo: canRedo); |
155 | } |
156 | } |
157 | |
158 | void _undoFromIntent(UndoTextIntent intent) { |
159 | undo(); |
160 | } |
161 | |
162 | void _redoFromIntent(RedoTextIntent intent) { |
163 | redo(); |
164 | } |
165 | |
166 | void _update(T? nextValue) { |
167 | if (nextValue == null) { |
168 | return; |
169 | } |
170 | if (nextValue == _lastValue) { |
171 | return; |
172 | } |
173 | _lastValue = nextValue; |
174 | _duringTrigger = true; |
175 | try { |
176 | widget.onTriggered(nextValue); |
177 | assert(widget.value.value == nextValue); |
178 | } finally { |
179 | _duringTrigger = false; |
180 | } |
181 | } |
182 | |
183 | void _push() { |
184 | if (widget.value.value == _lastValue) { |
185 | return; |
186 | } |
187 | |
188 | if (_duringTrigger) { |
189 | return; |
190 | } |
191 | |
192 | if (!(widget.shouldChangeUndoStack?.call(_lastValue, widget.value.value) ?? true)) { |
193 | return; |
194 | } |
195 | |
196 | final T nextValue = widget.undoStackModifier?.call(widget.value.value) ?? widget.value.value; |
197 | if (nextValue == _lastValue) { |
198 | return; |
199 | } |
200 | |
201 | _lastValue = nextValue; |
202 | |
203 | _throttleTimer = _throttledPush(nextValue); |
204 | } |
205 | |
206 | void _handleFocus() { |
207 | if (!widget.focusNode.hasFocus) { |
208 | if (UndoManager.client == this) { |
209 | UndoManager.client = null; |
210 | } |
211 | |
212 | return; |
213 | } |
214 | UndoManager.client = this; |
215 | _updateState(); |
216 | } |
217 | |
218 | @override |
219 | void handlePlatformUndo(UndoDirection direction) { |
220 | switch (direction) { |
221 | case UndoDirection.undo: |
222 | undo(); |
223 | case UndoDirection.redo: |
224 | redo(); |
225 | } |
226 | } |
227 | |
228 | @override |
229 | void initState() { |
230 | super.initState(); |
231 | _throttledPush = _throttle<T>( |
232 | duration: _kThrottleDuration, |
233 | function: (T currentValue) { |
234 | _stack.push(currentValue); |
235 | _updateState(); |
236 | }, |
237 | ); |
238 | _push(); |
239 | widget.value.addListener(_push); |
240 | _handleFocus(); |
241 | widget.focusNode.addListener(_handleFocus); |
242 | _effectiveController.onUndo.addListener(undo); |
243 | _effectiveController.onRedo.addListener(redo); |
244 | } |
245 | |
246 | @override |
247 | void didUpdateWidget(UndoHistory<T> oldWidget) { |
248 | super.didUpdateWidget(oldWidget); |
249 | if (widget.value != oldWidget.value) { |
250 | _stack.clear(); |
251 | oldWidget.value.removeListener(_push); |
252 | widget.value.addListener(_push); |
253 | } |
254 | if (widget.focusNode != oldWidget.focusNode) { |
255 | oldWidget.focusNode.removeListener(_handleFocus); |
256 | widget.focusNode.addListener(_handleFocus); |
257 | } |
258 | if (widget.controller != oldWidget.controller) { |
259 | _effectiveController.onUndo.removeListener(undo); |
260 | _effectiveController.onRedo.removeListener(redo); |
261 | _controller?.dispose(); |
262 | _controller = null; |
263 | _effectiveController.onUndo.addListener(undo); |
264 | _effectiveController.onRedo.addListener(redo); |
265 | } |
266 | } |
267 | |
268 | @override |
269 | void dispose() { |
270 | if (UndoManager.client == this) { |
271 | UndoManager.client = null; |
272 | } |
273 | |
274 | widget.value.removeListener(_push); |
275 | widget.focusNode.removeListener(_handleFocus); |
276 | _effectiveController.onUndo.removeListener(undo); |
277 | _effectiveController.onRedo.removeListener(redo); |
278 | _controller?.dispose(); |
279 | _throttleTimer?.cancel(); |
280 | super.dispose(); |
281 | } |
282 | |
283 | @override |
284 | Widget build(BuildContext context) { |
285 | return Actions( |
286 | actions: <Type, Action<Intent>>{ |
287 | UndoTextIntent: Action<UndoTextIntent>.overridable(context: context, defaultAction: CallbackAction<UndoTextIntent>(onInvoke: _undoFromIntent)), |
288 | RedoTextIntent: Action<RedoTextIntent>.overridable(context: context, defaultAction: CallbackAction<RedoTextIntent>(onInvoke: _redoFromIntent)), |
289 | }, |
290 | child: widget.child, |
291 | ); |
292 | } |
293 | } |
294 | |
295 | /// Represents whether the current undo stack can undo or redo. |
296 | @immutable |
297 | class UndoHistoryValue { |
298 | /// Creates a value for whether the current undo stack can undo or redo. |
299 | /// |
300 | /// The [canUndo] and [canRedo] arguments must have a value, but default to |
301 | /// false. |
302 | const UndoHistoryValue({this.canUndo = false, this.canRedo = false}); |
303 | |
304 | /// A value corresponding to an undo stack that can neither undo nor redo. |
305 | static const UndoHistoryValue empty = UndoHistoryValue(); |
306 | |
307 | /// Whether the current undo stack can perform an undo operation. |
308 | final bool canUndo; |
309 | |
310 | /// Whether the current undo stack can perform a redo operation. |
311 | final bool canRedo; |
312 | |
313 | @override |
314 | String toString() => ' ${objectRuntimeType(this, 'UndoHistoryValue' )}(canUndo: $canUndo, canRedo: $canRedo)' ; |
315 | |
316 | @override |
317 | bool operator ==(Object other) { |
318 | if (identical(this, other)) { |
319 | return true; |
320 | } |
321 | return other is UndoHistoryValue && other.canUndo == canUndo && other.canRedo == canRedo; |
322 | } |
323 | |
324 | @override |
325 | int get hashCode => Object.hash( |
326 | canUndo.hashCode, |
327 | canRedo.hashCode, |
328 | ); |
329 | } |
330 | |
331 | /// A controller for the undo history, for example for an editable text field. |
332 | /// |
333 | /// Whenever a change happens to the underlying value that the [UndoHistory] |
334 | /// widget tracks, that widget updates the [value] and the controller notifies |
335 | /// it's listeners. Listeners can then read the canUndo and canRedo |
336 | /// properties of the value to discover whether [undo] or [redo] are possible. |
337 | /// |
338 | /// The controller also has [undo] and [redo] methods to modify the undo |
339 | /// history. |
340 | /// |
341 | /// {@tool dartpad} |
342 | /// This example creates a [TextField] with an [UndoHistoryController] |
343 | /// which provides undo and redo buttons. |
344 | /// |
345 | /// ** See code in examples/api/lib/widgets/undo_history/undo_history_controller.0.dart ** |
346 | /// {@end-tool} |
347 | /// |
348 | /// See also: |
349 | /// |
350 | /// * [EditableText], which uses the [UndoHistory] widget and allows |
351 | /// control of the underlying history using an [UndoHistoryController]. |
352 | class UndoHistoryController extends ValueNotifier<UndoHistoryValue> { |
353 | /// Creates a controller for an [UndoHistory] widget. |
354 | UndoHistoryController({UndoHistoryValue? value}) : super(value ?? UndoHistoryValue.empty); |
355 | |
356 | /// Notifies listeners that [undo] has been called. |
357 | final ChangeNotifier onUndo = ChangeNotifier(); |
358 | |
359 | /// Notifies listeners that [redo] has been called. |
360 | final ChangeNotifier onRedo = ChangeNotifier(); |
361 | |
362 | /// Reverts the value on the stack to the previous value. |
363 | void undo() { |
364 | if (!value.canUndo) { |
365 | return; |
366 | } |
367 | |
368 | onUndo.notifyListeners(); |
369 | } |
370 | |
371 | /// Updates the value on the stack to the next value. |
372 | void redo() { |
373 | if (!value.canRedo) { |
374 | return; |
375 | } |
376 | |
377 | onRedo.notifyListeners(); |
378 | } |
379 | |
380 | @override |
381 | void dispose() { |
382 | onUndo.dispose(); |
383 | onRedo.dispose(); |
384 | super.dispose(); |
385 | } |
386 | } |
387 | |
388 | /// A data structure representing a chronological list of states that can be |
389 | /// undone and redone. |
390 | class _UndoStack<T> { |
391 | /// Creates an instance of [_UndoStack]. |
392 | _UndoStack(); |
393 | |
394 | final List<T> _list = <T>[]; |
395 | |
396 | // The index of the current value, or -1 if the list is empty. |
397 | int _index = -1; |
398 | |
399 | /// Returns the current value of the stack. |
400 | T? get currentValue => _list.isEmpty ? null : _list[_index]; |
401 | |
402 | bool get canUndo => _list.isNotEmpty && _index > 0; |
403 | |
404 | bool get canRedo => _list.isNotEmpty && _index < _list.length - 1; |
405 | |
406 | /// Add a new state change to the stack. |
407 | /// |
408 | /// Pushing identical objects will not create multiple entries. |
409 | void push(T value) { |
410 | if (_list.isEmpty) { |
411 | _index = 0; |
412 | _list.add(value); |
413 | return; |
414 | } |
415 | |
416 | assert(_index < _list.length && _index >= 0); |
417 | |
418 | if (value == currentValue) { |
419 | return; |
420 | } |
421 | |
422 | // If anything has been undone in this stack, remove those irrelevant states |
423 | // before adding the new one. |
424 | if (_index != _list.length - 1) { |
425 | _list.removeRange(_index + 1, _list.length); |
426 | } |
427 | _list.add(value); |
428 | _index = _list.length - 1; |
429 | } |
430 | |
431 | /// Returns the current value after an undo operation. |
432 | /// |
433 | /// An undo operation moves the current value to the previously pushed value, |
434 | /// if any. |
435 | /// |
436 | /// Iff the stack is completely empty, then returns null. |
437 | T? undo() { |
438 | if (_list.isEmpty) { |
439 | return null; |
440 | } |
441 | |
442 | assert(_index < _list.length && _index >= 0); |
443 | |
444 | if (_index != 0) { |
445 | _index = _index - 1; |
446 | } |
447 | |
448 | return currentValue; |
449 | } |
450 | |
451 | /// Returns the current value after a redo operation. |
452 | /// |
453 | /// A redo operation moves the current value to the value that was last |
454 | /// undone, if any. |
455 | /// |
456 | /// Iff the stack is completely empty, then returns null. |
457 | T? redo() { |
458 | if (_list.isEmpty) { |
459 | return null; |
460 | } |
461 | |
462 | assert(_index < _list.length && _index >= 0); |
463 | |
464 | if (_index < _list.length - 1) { |
465 | _index = _index + 1; |
466 | } |
467 | |
468 | return currentValue; |
469 | } |
470 | |
471 | /// Remove everything from the stack. |
472 | void clear() { |
473 | _list.clear(); |
474 | _index = -1; |
475 | } |
476 | |
477 | @override |
478 | String toString() { |
479 | return '_UndoStack $_list' ; |
480 | } |
481 | } |
482 | |
483 | /// A function that can be throttled with the throttle function. |
484 | typedef _Throttleable<T> = void Function(T currentArg); |
485 | |
486 | /// A function that has been throttled by [_throttle]. |
487 | typedef _Throttled<T> = Timer Function(T currentArg); |
488 | |
489 | /// Returns a _Throttled that will call through to the given function only a |
490 | /// maximum of once per duration. |
491 | /// |
492 | /// Only works for functions that take exactly one argument and return void. |
493 | _Throttled<T> _throttle<T>({ |
494 | required Duration duration, |
495 | required _Throttleable<T> function, |
496 | }) { |
497 | Timer? timer; |
498 | late T arg; |
499 | |
500 | return (T currentArg) { |
501 | arg = currentArg; |
502 | if (timer != null && timer!.isActive) { |
503 | return timer!; |
504 | } |
505 | timer = Timer(duration, () { |
506 | function(arg); |
507 | timer = null; |
508 | }); |
509 | return timer!; |
510 | }; |
511 | } |
512 | |