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 =>
115 widget.controller ?? (_controller ??= UndoHistoryController());
116
117 @override
118 void undo() {
119 if (_stack.currentValue == null) {
120 // Returns early if there is not a first value registered in the history.
121 // This is important because, if an undo is received while the initial
122 // value is being pushed (a.k.a when the field gets the focus but the
123 // throttling delay is pending), the initial push should not be canceled.
124 return;
125 }
126 if (_throttleTimer?.isActive ?? false) {
127 _throttleTimer?.cancel(); // Cancel ongoing push, if any.
128 _update(_stack.currentValue);
129 } else {
130 _update(_stack.undo());
131 }
132 _updateState();
133 }
134
135 @override
136 void redo() {
137 _update(_stack.redo());
138 _updateState();
139 }
140
141 @override
142 bool get canUndo => _stack.canUndo;
143
144 @override
145 bool get canRedo => _stack.canRedo;
146
147 void _updateState() {
148 _effectiveController.value = UndoHistoryValue(canUndo: canUndo, canRedo: canRedo);
149
150 if (defaultTargetPlatform != TargetPlatform.iOS) {
151 return;
152 }
153
154 if (UndoManager.client == this) {
155 UndoManager.setUndoState(canUndo: canUndo, canRedo: canRedo);
156 }
157 }
158
159 void _undoFromIntent(UndoTextIntent intent) {
160 undo();
161 }
162
163 void _redoFromIntent(RedoTextIntent intent) {
164 redo();
165 }
166
167 void _update(T? nextValue) {
168 if (nextValue == null) {
169 return;
170 }
171 if (nextValue == _lastValue) {
172 return;
173 }
174 _lastValue = nextValue;
175 _duringTrigger = true;
176 try {
177 widget.onTriggered(nextValue);
178 assert(widget.value.value == nextValue);
179 } finally {
180 _duringTrigger = false;
181 }
182 }
183
184 void _push() {
185 if (widget.value.value == _lastValue) {
186 return;
187 }
188
189 if (_duringTrigger) {
190 return;
191 }
192
193 if (!(widget.shouldChangeUndoStack?.call(_lastValue, widget.value.value) ?? true)) {
194 return;
195 }
196
197 final T nextValue = widget.undoStackModifier?.call(widget.value.value) ?? widget.value.value;
198 if (nextValue == _lastValue) {
199 return;
200 }
201
202 _lastValue = nextValue;
203
204 _throttleTimer = _throttledPush(nextValue);
205 }
206
207 void _handleFocus() {
208 if (!widget.focusNode.hasFocus) {
209 if (UndoManager.client == this) {
210 UndoManager.client = null;
211 }
212
213 return;
214 }
215 UndoManager.client = this;
216 _updateState();
217 }
218
219 @override
220 void handlePlatformUndo(UndoDirection direction) {
221 switch (direction) {
222 case UndoDirection.undo:
223 undo();
224 case UndoDirection.redo:
225 redo();
226 }
227 }
228
229 @protected
230 @override
231 void initState() {
232 super.initState();
233 _throttledPush = _throttle<T>(
234 duration: _kThrottleDuration,
235 function: (T currentValue) {
236 _stack.push(currentValue);
237 _updateState();
238 },
239 );
240 _push();
241 widget.value.addListener(_push);
242 _handleFocus();
243 widget.focusNode.addListener(_handleFocus);
244 _effectiveController.onUndo.addListener(undo);
245 _effectiveController.onRedo.addListener(redo);
246 }
247
248 @protected
249 @override
250 void didUpdateWidget(UndoHistory<T> oldWidget) {
251 super.didUpdateWidget(oldWidget);
252 if (widget.value != oldWidget.value) {
253 _stack.clear();
254 oldWidget.value.removeListener(_push);
255 widget.value.addListener(_push);
256 }
257 if (widget.focusNode != oldWidget.focusNode) {
258 oldWidget.focusNode.removeListener(_handleFocus);
259 widget.focusNode.addListener(_handleFocus);
260 }
261 if (widget.controller != oldWidget.controller) {
262 _effectiveController.onUndo.removeListener(undo);
263 _effectiveController.onRedo.removeListener(redo);
264 _controller?.dispose();
265 _controller = null;
266 _effectiveController.onUndo.addListener(undo);
267 _effectiveController.onRedo.addListener(redo);
268 }
269 }
270
271 @protected
272 @override
273 void dispose() {
274 if (UndoManager.client == this) {
275 UndoManager.client = null;
276 }
277
278 widget.value.removeListener(_push);
279 widget.focusNode.removeListener(_handleFocus);
280 _effectiveController.onUndo.removeListener(undo);
281 _effectiveController.onRedo.removeListener(redo);
282 _controller?.dispose();
283 _throttleTimer?.cancel();
284 super.dispose();
285 }
286
287 @protected
288 @override
289 Widget build(BuildContext context) {
290 return Actions(
291 actions: <Type, Action<Intent>>{
292 UndoTextIntent: Action<UndoTextIntent>.overridable(
293 context: context,
294 defaultAction: CallbackAction<UndoTextIntent>(onInvoke: _undoFromIntent),
295 ),
296 RedoTextIntent: Action<RedoTextIntent>.overridable(
297 context: context,
298 defaultAction: CallbackAction<RedoTextIntent>(onInvoke: _redoFromIntent),
299 ),
300 },
301 child: widget.child,
302 );
303 }
304}
305
306/// Represents whether the current undo stack can undo or redo.
307@immutable
308class UndoHistoryValue {
309 /// Creates a value for whether the current undo stack can undo or redo.
310 ///
311 /// The [canUndo] and [canRedo] arguments must have a value, but default to
312 /// false.
313 const UndoHistoryValue({this.canUndo = false, this.canRedo = false});
314
315 /// A value corresponding to an undo stack that can neither undo nor redo.
316 static const UndoHistoryValue empty = UndoHistoryValue();
317
318 /// Whether the current undo stack can perform an undo operation.
319 final bool canUndo;
320
321 /// Whether the current undo stack can perform a redo operation.
322 final bool canRedo;
323
324 @override
325 String toString() =>
326 '${objectRuntimeType(this, 'UndoHistoryValue')}(canUndo: $canUndo, canRedo: $canRedo)';
327
328 @override
329 bool operator ==(Object other) {
330 if (identical(this, other)) {
331 return true;
332 }
333 return other is UndoHistoryValue && other.canUndo == canUndo && other.canRedo == canRedo;
334 }
335
336 @override
337 int get hashCode => Object.hash(canUndo.hashCode, canRedo.hashCode);
338}
339
340/// A controller for the undo history, for example for an editable text field.
341///
342/// Whenever a change happens to the underlying value that the [UndoHistory]
343/// widget tracks, that widget updates the [value] and the controller notifies
344/// it's listeners. Listeners can then read the canUndo and canRedo
345/// properties of the value to discover whether [undo] or [redo] are possible.
346///
347/// The controller also has [undo] and [redo] methods to modify the undo
348/// history.
349///
350/// {@tool dartpad}
351/// This example creates a [TextField] with an [UndoHistoryController]
352/// which provides undo and redo buttons.
353///
354/// ** See code in examples/api/lib/widgets/undo_history/undo_history_controller.0.dart **
355/// {@end-tool}
356///
357/// See also:
358///
359/// * [EditableText], which uses the [UndoHistory] widget and allows
360/// control of the underlying history using an [UndoHistoryController].
361class UndoHistoryController extends ValueNotifier<UndoHistoryValue> {
362 /// Creates a controller for an [UndoHistory] widget.
363 UndoHistoryController({UndoHistoryValue? value}) : super(value ?? UndoHistoryValue.empty);
364
365 /// Notifies listeners that [undo] has been called.
366 final ChangeNotifier onUndo = ChangeNotifier();
367
368 /// Notifies listeners that [redo] has been called.
369 final ChangeNotifier onRedo = ChangeNotifier();
370
371 /// Reverts the value on the stack to the previous value.
372 void undo() {
373 if (!value.canUndo) {
374 return;
375 }
376
377 onUndo.notifyListeners();
378 }
379
380 /// Updates the value on the stack to the next value.
381 void redo() {
382 if (!value.canRedo) {
383 return;
384 }
385
386 onRedo.notifyListeners();
387 }
388
389 @override
390 void dispose() {
391 onUndo.dispose();
392 onRedo.dispose();
393 super.dispose();
394 }
395}
396
397/// A data structure representing a chronological list of states that can be
398/// undone and redone.
399class _UndoStack<T> {
400 /// Creates an instance of [_UndoStack].
401 _UndoStack();
402
403 final List<T> _list = <T>[];
404
405 // The index of the current value, or -1 if the list is empty.
406 int _index = -1;
407
408 /// Returns the current value of the stack.
409 T? get currentValue => _list.isEmpty ? null : _list[_index];
410
411 bool get canUndo => _list.isNotEmpty && _index > 0;
412
413 bool get canRedo => _list.isNotEmpty && _index < _list.length - 1;
414
415 /// Add a new state change to the stack.
416 ///
417 /// Pushing identical objects will not create multiple entries.
418 void push(T value) {
419 if (_list.isEmpty) {
420 _index = 0;
421 _list.add(value);
422 return;
423 }
424
425 assert(_index < _list.length && _index >= 0);
426
427 if (value == currentValue) {
428 return;
429 }
430
431 // If anything has been undone in this stack, remove those irrelevant states
432 // before adding the new one.
433 if (_index != _list.length - 1) {
434 _list.removeRange(_index + 1, _list.length);
435 }
436 _list.add(value);
437 _index = _list.length - 1;
438 }
439
440 /// Returns the current value after an undo operation.
441 ///
442 /// An undo operation moves the current value to the previously pushed value,
443 /// if any.
444 ///
445 /// Iff the stack is completely empty, then returns null.
446 T? undo() {
447 if (_list.isEmpty) {
448 return null;
449 }
450
451 assert(_index < _list.length && _index >= 0);
452
453 if (_index != 0) {
454 _index = _index - 1;
455 }
456
457 return currentValue;
458 }
459
460 /// Returns the current value after a redo operation.
461 ///
462 /// A redo operation moves the current value to the value that was last
463 /// undone, if any.
464 ///
465 /// Iff the stack is completely empty, then returns null.
466 T? redo() {
467 if (_list.isEmpty) {
468 return null;
469 }
470
471 assert(_index < _list.length && _index >= 0);
472
473 if (_index < _list.length - 1) {
474 _index = _index + 1;
475 }
476
477 return currentValue;
478 }
479
480 /// Remove everything from the stack.
481 void clear() {
482 _list.clear();
483 _index = -1;
484 }
485
486 @override
487 String toString() {
488 return '_UndoStack $_list';
489 }
490}
491
492/// A function that can be throttled with the throttle function.
493typedef _Throttleable<T> = void Function(T currentArg);
494
495/// A function that has been throttled by [_throttle].
496typedef _Throttled<T> = Timer Function(T currentArg);
497
498/// Returns a _Throttled that will call through to the given function only a
499/// maximum of once per duration.
500///
501/// Only works for functions that take exactly one argument and return void.
502_Throttled<T> _throttle<T>({required Duration duration, required _Throttleable<T> function}) {
503 Timer? timer;
504 late T arg;
505
506 return (T currentArg) {
507 arg = currentArg;
508 if (timer != null && timer!.isActive) {
509 return timer!;
510 }
511 timer = Timer(duration, () {
512 function(arg);
513 timer = null;
514 });
515 return timer!;
516 };
517}
518