Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Flutter code sample for UndoHistoryController.

import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

static const String _title = 'Flutter Code Sample';

@override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyHomePage(),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
final TextEditingController _controller = TextEditingController();
final FocusNode _focusNode = FocusNode();
final UndoHistoryController _undoController = UndoHistoryController();

TextStyle? get enabledStyle => Theme.of(context).textTheme.bodyMedium;
TextStyle? get disabledStyle => Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey);

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
maxLines: 4,
controller: _controller,
focusNode: _focusNode,
undoController: _undoController,
),
ValueListenableBuilder<UndoHistoryValue>(
valueListenable: _undoController,
builder: (BuildContext context, UndoHistoryValue value, Widget? child) {
return Row(
children: <Widget>[
TextButton(
child: Text('Undo', style: value.canUndo ? enabledStyle : disabledStyle),
onPressed: () {
_undoController.undo();
},
),
TextButton(
child: Text('Redo', style: value.canRedo ? enabledStyle : disabledStyle),
onPressed: () {
_undoController.redo();
},
),
],
);
},
),
],
),
),
);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding a full example! Even just for me as a reviewer it helps me understand this PR.

1 change: 1 addition & 0 deletions packages/flutter/lib/services.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ export 'src/services/text_editing_delta.dart';
export 'src/services/text_formatter.dart';
export 'src/services/text_input.dart';
export 'src/services/text_layout_metrics.dart';
export 'src/services/undo_manager.dart';
7 changes: 7 additions & 0 deletions packages/flutter/lib/src/cupertino/text_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ class CupertinoTextField extends StatefulWidget {
super.key,
this.controller,
this.focusNode,
this.undoController,
this.decoration = _kDefaultRoundedBorderDecoration,
this.padding = const EdgeInsets.all(7.0),
this.placeholder,
Expand Down Expand Up @@ -347,6 +348,7 @@ class CupertinoTextField extends StatefulWidget {
super.key,
this.controller,
this.focusNode,
this.undoController,
this.decoration,
this.padding = const EdgeInsets.all(7.0),
this.placeholder,
Expand Down Expand Up @@ -780,6 +782,9 @@ class CupertinoTextField extends StatefulWidget {
decorationStyle: TextDecorationStyle.dotted,
);

/// {@macro flutter.widgets.undoHistory.controller}
final UndoHistoryController? undoController;

@override
State<CupertinoTextField> createState() => _CupertinoTextFieldState();

Expand All @@ -788,6 +793,7 @@ class CupertinoTextField extends StatefulWidget {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<TextEditingController>('controller', controller, defaultValue: null));
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
properties.add(DiagnosticsProperty<UndoHistoryController>('undoController', undoController, defaultValue: null));
properties.add(DiagnosticsProperty<BoxDecoration>('decoration', decoration));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding));
properties.add(StringProperty('placeholder', placeholder));
Expand Down Expand Up @@ -1277,6 +1283,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
child: EditableText(
key: editableTextKey,
controller: controller,
undoController: widget.undoController,
readOnly: widget.readOnly,
toolbarOptions: widget.toolbarOptions,
showCursor: widget.showCursor,
Expand Down
6 changes: 6 additions & 0 deletions packages/flutter/lib/src/material/text_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ class TextField extends StatefulWidget {
super.key,
this.controller,
this.focusNode,
this.undoController,
this.decoration = const InputDecoration(),
TextInputType? keyboardType,
this.textInputAction,
Expand Down Expand Up @@ -774,6 +775,9 @@ class TextField extends StatefulWidget {
/// be possible to move the focus to the text field with tab key.
final bool canRequestFocus;

/// {@macro flutter.widgets.undoHistory.controller}
final UndoHistoryController? undoController;

static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
return AdaptiveTextSelectionToolbar.editableText(
editableTextState: editableTextState,
Expand Down Expand Up @@ -834,6 +838,7 @@ class TextField extends StatefulWidget {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<TextEditingController>('controller', controller, defaultValue: null));
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
properties.add(DiagnosticsProperty<UndoHistoryController>('undoController', undoController, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null));
properties.add(DiagnosticsProperty<InputDecoration>('decoration', decoration, defaultValue: const InputDecoration()));
properties.add(DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: TextInputType.text));
Expand Down Expand Up @@ -1313,6 +1318,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
showSelectionHandles: _showSelectionHandles,
controller: controller,
focusNode: focusNode,
undoController: widget.undoController,
keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
textCapitalization: widget.textCapitalization,
Expand Down
6 changes: 6 additions & 0 deletions packages/flutter/lib/src/services/system_channels.dart
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,12 @@ class SystemChannels {
'flutter/spellcheck',
);

/// A JSON [MethodChannel] for handling undo events.
static const MethodChannel undoManager = OptionalMethodChannel(
'flutter/undomanager',
JSONMethodCodec(),
);

/// A JSON [BasicMessageChannel] for keyboard events.
///
/// Each incoming message received on this channel (registered using
Expand Down
131 changes: 131 additions & 0 deletions packages/flutter/lib/src/services/undo_manager.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart';

import '../../services.dart';

/// The direction in which an undo action should be performed, whether undo or redo.
enum UndoDirection {
/// Perform an undo action.
undo,

/// Perform a redo action.
redo
}

/// A low-level interface to the system's undo manager.
///
/// To receive events from the system undo manager, create an
/// [UndoManagerClient] and set it as the [client] on [UndoManager].
///
/// The [setUndoState] method can be used to update the system's undo manager
/// using the [canUndo] and [canRedo] parameters.
///
/// When the system undo or redo button is tapped, the current
/// [UndoManagerClient] will receive [UndoManagerClient.handlePlatformUndo]
/// with an [UndoDirection] representing whether the event is "undo" or "redo".
///
/// Currently, only iOS has an UndoManagerPlugin implemented on the engine side.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Maybe add the following. I'm looking at these docs and noticing that someone that lands on this page might not know what this class can actually be used for at a high level.

"On iOS, this can be used to listen to the keyboard undo/redo buttons and the undo/redo gestures."

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have an issue to also add support for macos?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Someone else should double check me on this, but it seems to work on macOS without any further changes, I think because we're handling the UndoTextIntent and RedoTextIntent.

Kapture 2022-11-08 at 11 59 53

/// On iOS, this can be used to listen to the keyboard undo/redo buttons and the
/// undo/redo gestures.
///
/// See also:
///
/// * [NSUndoManager](https://developer.apple.com/documentation/foundation/nsundomanager)
class UndoManager {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Add a "See also" with a reference to the Apple docs.

UndoManager._() {
_channel = SystemChannels.undoManager;
_channel.setMethodCallHandler(_handleUndoManagerInvocation);
}

/// Set the [MethodChannel] used to communicate with the system's undo manager.
///
/// This is only meant for testing within the Flutter SDK. Changing this
/// will break the ability to set the undo status or receive undo and redo
/// events from the system. This has no effect if asserts are disabled.
@visibleForTesting
static void setChannel(MethodChannel newChannel) {
assert(() {
_instance._channel = newChannel..setMethodCallHandler(_instance._handleUndoManagerInvocation);
return true;
}());
}

static final UndoManager _instance = UndoManager._();

/// Receive undo and redo events from the system's [UndoManager].
///
/// Setting the [client] will cause [UndoManagerClient.handlePlatformUndo]
/// to be called when a system undo or redo is triggered, such as by tapping
/// the undo/redo keyboard buttons or using the 3-finger swipe gestures.
static set client(UndoManagerClient? client) {
_instance._currentClient = client;
}

/// Return the current [UndoManagerClient].
static UndoManagerClient? get client => _instance._currentClient;

/// Set the current state of the system UndoManager. [canUndo] and [canRedo]
/// control the respective "undo" and "redo" buttons of the system UndoManager.
static void setUndoState({bool canUndo = false, bool canRedo = false}) {
_instance._setUndoState(canUndo: canUndo, canRedo: canRedo);
}

late MethodChannel _channel;

UndoManagerClient? _currentClient;

Future<dynamic> _handleUndoManagerInvocation(MethodCall methodCall) async {
final String method = methodCall.method;
final List<dynamic> args = methodCall.arguments as List<dynamic>;
if (method == 'UndoManagerClient.handleUndo') {
assert(_currentClient != null, 'There must be a current UndoManagerClient.');
_currentClient!.handlePlatformUndo(_toUndoDirection(args[0] as String));

return;
}

throw MissingPluginException();
}

void _setUndoState({bool canUndo = false, bool canRedo = false}) {
_channel.invokeMethod<void>(
'UndoManager.setUndoState',
<String, bool>{'canUndo': canUndo, 'canRedo': canRedo}
);
}

UndoDirection _toUndoDirection(String direction) {
switch (direction) {
case 'undo':
return UndoDirection.undo;
case 'redo':
return UndoDirection.redo;
}
throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Unknown undo direction: $direction')]);
}
}

/// An interface to receive events from a native UndoManager.
mixin UndoManagerClient {
/// Requests that the client perform an undo or redo operation.
///
/// Currently only used on iOS 9+ when the undo or redo methods are invoked
/// by the platform. For example, when using three-finger swipe gestures,
/// the iPad keyboard, or voice control.
void handlePlatformUndo(UndoDirection direction);

/// Reverts the value on the stack to the previous value.
void undo();

/// Updates the value on the stack to the next value.
void redo();

/// Will be true if there are past values on the stack.
bool get canUndo;

/// Will be true if there are future values on the stack.
bool get canRedo;
}
Loading