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