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';
9library;
10
11import 'dart:async';
12
13import 'package:flutter/foundation.dart';
14import 'package:flutter/services.dart';
15
16import 'actions.dart';
17import 'focus_manager.dart';
18import 'framework.dart';
19import '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.
33class 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
96class 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
297class 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].
352class 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.
390class _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.
484typedef _Throttleable<T> = void Function(T currentArg);
485
486/// A function that has been throttled by [_throttle].
487typedef _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