-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Add support for iOS UndoManager #98294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2c73403
d9cd837
a669525
1e30322
4f2c2cd
60e15c5
3ee9fef
b899d85
f67fef9
7f97974
b74a5db
403da3d
a26bd35
c5e7c09
722e226
8abe59b
49ca399
50d96f9
deec8c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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(); | ||
| }, | ||
| ), | ||
| ], | ||
| ); | ||
| }, | ||
| ), | ||
| ], | ||
| ), | ||
| ), | ||
| ); | ||
| } | ||
| } | ||
| 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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."
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have an issue to also add support for macos?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| /// 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; | ||
| } | ||

There was a problem hiding this comment.
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.