Skip to content

Commit

Permalink
Add support for iOS UndoManager (#98294)
Browse files Browse the repository at this point in the history
Add support for iOS UndoManager
  • Loading branch information
fbcouch authored Mar 8, 2023
1 parent a16e620 commit 2a67bf7
Show file tree
Hide file tree
Showing 14 changed files with 1,429 additions and 347 deletions.
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();
},
),
],
);
},
),
],
),
),
);
}
}
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.
/// 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 {
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

0 comments on commit 2a67bf7

Please sign in to comment.