Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Merged
merged 19 commits into from
Mar 8, 2023

Conversation

fbcouch
Copy link
Contributor

@fbcouch fbcouch commented Feb 11, 2022

This PR is modifies the framework to allow the Undo/Redo support added by _TextEditingHistory to interact with the iOS UndoManager. That should enable 3-finger/shake gestures to undo/redo text editing and enable undo/redo from the iPad keyboard.

I don't love this approach, since it feels a little bit hacky and requires a breaking change to the TextInputClient interface. It's possible #98280 could enable a better way of doing this.

I haven't had a chance to test this out – still waiting for the engine to compile, but wanted to plant the flag on this and possibly feedback on the approach.

Related to #34749 and #77614

Engine PR: flutter/engine#31415

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@flutter-dashboard flutter-dashboard bot added a: text input Entering text in a text field or keyboard related problems framework flutter/packages/flutter repository. See also f: labels. labels Feb 11, 2022
@fbcouch
Copy link
Contributor Author

fbcouch commented Feb 14, 2022

I believe this is a reasonable approach – I think it may make sense to swap over to something along the lines of setUndoManagerState, though, similar to updateEditingState, where the engine can just set up the undoManager as needed, rather than having the framework manage it through three separate calls.

@flutter-dashboard flutter-dashboard bot added the a: tests "flutter test", flutter_test, or one of our tests label Feb 14, 2022
@chunhtai chunhtai self-requested a review February 15, 2022 18:01
@fbcouch
Copy link
Contributor Author

fbcouch commented Feb 16, 2022

@justinmc I'd like to get your thoughts on the framework side of things – right now I'm adding a method to TextInputClient, which will make this a breaking change. I'm wondering if it would be better to add a property to the TextInputConnection for some kind of new UndoDelegate, and have the TextEditingHistory widget set itself as the current undo delegate on the connection when it receives focus. That way we could avoid that breaking change. Does that make sense?

@fbcouch fbcouch marked this pull request as ready for review February 16, 2022 17:20
Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

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

Just to throw out a crazy idea: What if everything in the engine through to the platform message channel were separate from text editing? Since NSUndoManager itself is not specific to text editing, hypothetically this could be generic to any kind of undo/redo. In EditableText you would specifically use it for text editing. Just a thought I had, not necessarily a good idea.

Otherwise I like the idea you mentioned of an "UndoDelegate". Kind of similar to DeltaTextInputClient?

return;
}

WidgetsBinding.instance.addPostFrameCallback((_) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is the post frame callback needed?

@@ -4391,6 +4466,10 @@ class _UndoStack<T> {
/// Returns the current value of the stack.
T? get currentValue => _list.isEmpty ? null : _list[_index];

bool get canUndo => _list.isNotEmpty && _index > 0;

bool get canRedo => _list.isNotEmpty && _index < _list.length - 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing docs here and on canUndo above.

void handleKeyboardUndo(String direction) {
if (_editableKey.currentContext == null)
return;
if (direction == 'undo') {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Would it be better to send a boolean from the engine? It would save a little bit of space at least.

If not, maybe parse the string into an enum or compare it to constants.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, a boolean might be the best thing. I'm currently using an enum on the engine side, so I think just passing those values (0 and 1) instead of translating them to strings first might be the way to go here. Then instead of direction, this can be isRedo (or maybe split this into two methods, one to handle undo and one to handle redo)

@fbcouch
Copy link
Contributor Author

fbcouch commented Feb 17, 2022

Yes, I think the DeltaTextInputClient is a better version of what I was describing to avoid a breaking change...

Having everything go through a different channel is an interesting idea, for sure. Other than needing to do the weird ensureUndoEnabled trick on the engine side, everything else could be completely separate from the text editing plugin. I think that would make a lot of sense in the context of allowing programmatic control of Undo/Redo as well, so we could carve out a more generic undo/redo system where TextEditingHistory is just one implementer...I will give that some thought, for sure.

if (direction == 'undo') {
const UndoTextIntent intent = UndoTextIntent(SelectionChangedCause.keyboard);
final Action<UndoTextIntent>? action = Actions.maybeFind<UndoTextIntent>(_editableKey.currentContext!, intent: intent);
if (action != null)
Copy link
Contributor

Choose a reason for hiding this comment

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

this can be moved out of the both if(direction == 'undo') and else case so that we don't need to duplicate code

@fbcouch
Copy link
Contributor Author

fbcouch commented Feb 24, 2022

Thinking about this more, I really like the idea of separating this out from the text editing details. That way a drawing app could make use of it. I haven't had a chance to work on it this week (in the middle of moving), but I'm hoping to be back at it next week.

Does it make sense to still use a FocusNode to know which undo client should receive messages or should that be something left up to the implementers? I think FocusNodes make sense for text editing, but I'm not sure about drawing. I think it still could, but maybe best left to others to decide.

My initial thought is it would make sense to have some kind of UndoActions widget (not sure on the name) that takes an UndoController or some such that provides methods like canUndo, canRedo, undo() and redo(). That's where the question about focus nodes comes in...should UndoActions take a focusNode in order to automatically manage focus, or would it make more sense to have a method on UndoController to become the receiver of undo messages?

@fbcouch fbcouch marked this pull request as draft March 2, 2022 22:32
@fbcouch
Copy link
Contributor Author

fbcouch commented Mar 2, 2022

I converted this back to draft status for now, since this is a work in progress.

I added a new widget, UndoHistory, and a new service, UndoManager to handle the separate communication and make this more generic.

I haven't updated EditableText to use it yet, but here's an example app that I used to mock up an Undo/Redo UI around a TextField:

lib/main.dart
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  final TextEditingController _controller = TextEditingController();
  final FocusNode _focusNode = FocusNode();
  final GlobalKey<UndoHistoryState> _undoKey = GlobalKey<UndoHistoryState>();

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

  bool _canUndo = false, _canRedo = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            UndoHistory<TextEditingValue>(
              key: _undoKey,
              value: _controller,
              onTriggered: (value) {
                _controller.value = _controller.value.copyWith(text: value.text, selection: value.selection);
              },
              focusNode: _focusNode,
              shouldChangeUndoStack: (TextEditingValue? oldValue, TextEditingValue newValue) {
                if (oldValue == null) {
                  return true;
                }

                if (newValue == TextEditingValue.empty) {
                  return false;
                }

                return oldValue.text != newValue.text;
              },
              onUndoStackChanged: () => setState(() {
                _canUndo = _undoKey.currentState?.canUndo ?? false;
                _canRedo = _undoKey.currentState?.canRedo ?? false;
              }),
              child: TextField(
                maxLines: 4,
                controller: _controller,
                focusNode: _focusNode,
              ),
            ),
            Row(
              children: [
                TextButton(
                    child: Text('Undo', style: _canUndo ? enabledStyle : disabledStyle),
                    onPressed: () {
                      _undoKey.currentState?.undo();
                    }),
                TextButton(
                    child: Text('Redo', style: _canRedo ? enabledStyle : disabledStyle),
                    onPressed: () {
                      _undoKey.currentState?.redo();
                    }),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

@justinmc Let me know your thoughts on that approach. I'm also thinking it might be worth adding another level of indirection around which client is currently connected to the UndoManager, similar to TextInputConnection.

@justinmc
Copy link
Contributor

justinmc commented Mar 7, 2022

I like that approach, looks really reusable. Would that replace _TextEditingHistory or is it separate?

@fbcouch
Copy link
Contributor Author

fbcouch commented Mar 7, 2022

Cool, yes, I actually was able to get to the point of replacing _TextEditingHistory on Friday: https://github.com/flutter/flutter/pull/98294/files#diff-f5f93c879cef9a102adbf148583de9f3d9a05f9678fdcad7bde106f62200bb34R3193

There are a couple of things I don't love about the implementation right now, so let me know if you have thoughts on them:

  • I put the API to access the undo state on UndoHistoryState, but it's not very elegant to access from a TextField(key: _key) right now: (_key.currentState as TextSelectionGestureDetectorBuilderDelegate).editableTextKey.currentState?.undoHistory?.redo(). I'm wondering if providing more direct access and/or adding another interface on to _TextFieldState might make that simpler. UndoHistoryDelegate or something like that.
  • To provide software undo/redo buttons, I have to manually call setState after changes, so that might be something that either the UndoHistoryDelegate could provide a stream for, or there could be a stream at another level – maybe even on the UndoManager itself.

@fbcouch fbcouch marked this pull request as ready for review March 9, 2022 20:37
@flutter-dashboard flutter-dashboard bot added f: cupertino flutter/packages/flutter/cupertino repository f: material design flutter/packages/flutter/material repository. labels Mar 9, 2022
@fbcouch
Copy link
Contributor Author

fbcouch commented Mar 11, 2022

All right, I swapped this over to use a controller and I think it's a lot cleaner: https://github.com/flutter/flutter/pull/98294/files#diff-0dd319c779a725fd887d8a664e656ce5cf92b62934a821cf2f3815750c630245R56

Let me know how that looks!

@justinmc
Copy link
Contributor

CC @LongCatIsLooong who is probably also interested in this. We talked about the 3 finger swipe gesture, etc. before.

Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

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

A few quick comments here, I owe you a full review by Monday. Really excited for this PR.

// 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
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Period at the end.


static const String _title = 'Flutter Code Sample';

// This widget is the root of your application.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Remove this boilerplate comment.

),
);
}
}
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.

@@ -810,6 +812,9 @@ class CupertinoTextField extends StatefulWidget {
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final bool enableIMEPersonalizedLearning;

/// Controls the undo state.
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 use a template and macro for this repeated comment.

@@ -1261,6 +1261,13 @@ class TextInputConnection {
TextInput._instance._show();
}

/// Requests that the text input plugin set the undo or redo state.
///
/// This is currently only used on iOS 9+ to integrate with the native NSUndoManager.
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 a "See also" with a reference to UndoManager.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This actually needs to be removed since the TextInput channel is no longer used by this feature

/// with a string representing whether the event is "undo" or "redo".
///
/// Currently, only iOS has an UndoManagerPlugin implemented on the engine side.
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.

/// Receive undo and redo events from the system's UndoManager.
///
/// Setting the [client] will cause [UndoManagerClient.handleKeyboardUndo]
/// to be called when the system undo or redo buttons are tapped.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this separate from undo/redo gestures (3 finger swipe)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, that's a good point. I updated the comment to include the gestures.


/// A void function that takes a [TextEditingValue].
@visibleForTesting
typedef TextEditingValueCallback = void Function(TextEditingValue value);
Copy link
Contributor

Choose a reason for hiding this comment

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

Very low chance of a breaking change here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is that okay? Should we leave the typedef for now, just in case?

Copy link
Contributor

Choose a reason for hiding this comment

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

We should remove it if it's not used by the framework.

If it passes the customer tests and the Google tests then I think it's ok to try merging it. If it does break anything, then the migration process should be pretty simple (users should include their own typedef in their app I guess?).

@skia-gold
Copy link

Gold has detected about 1 new digest(s) on patchset 24.
View them at https://flutter-gold.skia.org/cl/github/98294

Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

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

Mostly just small things, but one point I want to make sure everyone agrees on is the addition of the new controller (UndoHistoryController). I left a comment about that below.

Otherwise I really like this approach and the fact that undo/redo isn't tied to text editing.

Also I think the Skia Gold failure is not a problem, maybe try pushing a merge commit and see if it goes away?

/// 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 handleKeyboardUndo(String direction);
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 handlePlatformUndo? Since it's not only a keyboard thing.

onTriggered: (TextEditingValue value) {
userUpdateTextEditingValue(value, SelectionChangedCause.keyboard);
},
shouldChangeUndoStack: (TextEditingValue? oldValue, TextEditingValue newValue) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I like this way of organizing UndoHistory 👍 . So this TextEditingValue-specific logic goes here, and UndoHistory can take a generic type parameter.


expect(client.latestMethodCall, isEmpty);

// Send handleUndo message with "undo" as the direction
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Periods at the end of these comments here and below.

});
}

class FakeUndoManagerClient implements UndoManagerClient {
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 make this private since it's not used anywhere outside of this file.

@@ -46,6 +47,68 @@ Offset textOffsetToPosition(WidgetTester tester, int offset) {
return endpoints[0].point + const Offset(0.0, -2.0);
}

Future<void> sendKeys(
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 a comment describing this function (even though I know there wasn't one before).

Copy link
Contributor

Choose a reason for hiding this comment

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

Another nit here: I think that should actually be a doc comment with 3 slashes ///.

Copy link
Contributor

Choose a reason for hiding this comment

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

By the way, I'm glad sendKeys will be reusable now. I've wanted it in other test files before.

///
/// * [EditableText], which uses the [UndoHistory] widget and allows
/// control of the underlying history using an [UndoHistoryController].
class UndoHistoryController extends ValueNotifier<UndoHistoryValue> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder what others think of this controller. It doesn't exactly follow the pattern that something like TextEditingController does because its value is UndoHistoryValue, not the actual value that is undone/redone. It kind of wraps the two ChangeNotifiers, but doesn't deal in a value itself.

Maybe that is a good thing...

Ultimately the purpose of this controller is to allow programmatic undo/redo, right? Other options:

  • Add this functionality to TextEditingController (though that's probably something we don't want to bloat).
  • List for an Intent? Undo/RedoIntent. Maybe too indirect and confusing.

This might already be the best solution as-is, but I just wanted to make sure we discuss it before committing to anything.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right that the UndoHistoryValue is more like a state than a value, so it is different from the TextEditingController in that way. That value is important for propagating the current undo/redo state back to the UI so that custom undo/redo interfaces can be built. The other two ChangeNotifiers are for sending events to allow programmatic undo/redo.

I don't love the idea of adding it to TextEditingController, or at least not only there, so that this can remain more generic: for example, someone could build a drawing interface with it and undo/redo lines on the drawing.

I could get on board with Intents, since we already use those for the keyboard shortcuts, though I agree that might be more confusing.

Copy link
Contributor

Choose a reason for hiding this comment

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

Reading through this again I think I'm onboard with UndoHistoryController as-is. It makes a lot of sense in the example you included in the examples directory.

@skia-gold
Copy link

Gold has detected about 1 new digest(s) on patchset 25.
View them at https://flutter-gold.skia.org/cl/github/98294

}

/// An interface to receive events from a native UndoManager.
abstract class UndoManagerClient {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm currently investigating whether or not we should use a mixin in cases like this instead of an abstract class (to avoid breaking users that extend this if/when a method is added). I'll let you know what I figure out and then I owe you a full review.

Copy link
Contributor

Choose a reason for hiding this comment

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

Here is the design doc. There's still some debate going on.

Copy link
Contributor

Choose a reason for hiding this comment

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

Update: Making this a mixin and using it via with instead of implements is typically preferred. Anytime a new method is added here in the future, giving it an empty implementation will avoid breaking changes. If breaking changes are desired, a method can be added with no implementation.

@fbcouch fbcouch force-pushed the feature/undo-redo-gestures branch from c094938 to 2c73403 Compare May 16, 2022 19:27
Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

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

@fbcouch Are you still available to get this PR ready to merge? Sorry for the hiatus while I figured out some platform channel stuff.

child: Text('Undo', style: value.canUndo ? enabledStyle : disabledStyle),
onPressed: () {
_undoController.undo();
}),
Copy link
Contributor

Choose a reason for hiding this comment

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

Here and below, split the closing }) onto two separate lines.


/// A low-level interface to the system's undo manager.
///
/// To receive events from the system UndoManager, create an
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: "UndoManager" => "undo manager" to match the previous paragraph?

Comment on lines 32 to 37
/// Set the [MethodChannel] used to communicate with the system's text input
/// control.
///
/// This is only meant for testing within the Flutter SDK. Changing this
/// will break the ability to input text. This has no effect if asserts are
/// disabled.
Copy link
Contributor

Choose a reason for hiding this comment

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

This sounds like TextInputClient, can you tweak them so they're about undo/redo?

}

/// An interface to receive events from a native UndoManager.
abstract class UndoManagerClient {
Copy link
Contributor

Choose a reason for hiding this comment

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

Update: Making this a mixin and using it via with instead of implements is typically preferred. Anytime a new method is added here in the future, giving it an empty implementation will avoid breaking changes. If breaking changes are desired, a method can be added with no implementation.

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

/// Will be false if there are future values on the stack.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this "false" be "true" or am I misreading it?

UndoHistoryController({UndoHistoryValue? value}) : super(value ?? UndoHistoryValue.empty);

/// Notifies listeners that [undo] has been called.
final ChangeNotifier onUndo = ChangeNotifier();
Copy link
Contributor

Choose a reason for hiding this comment

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

Should these ChangeNotifiers be disposed?

Copy link
Contributor

Choose a reason for hiding this comment

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

Can it be private? It seems to be a way an API just to wire up the undo/redo to the UndoHistory widget.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Possibly, but I think making it public makes this more useful to consumers, for example, if they are implementing their own version of UndoHistory, they would need to listen to the undo/redo notifications.

///
/// * [EditableText], which uses the [UndoHistory] widget and allows
/// control of the underlying history using an [UndoHistoryController].
class UndoHistoryController extends ValueNotifier<UndoHistoryValue> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Reading through this again I think I'm onboard with UndoHistoryController as-is. It makes a lot of sense in the example you included in the examples directory.

}

@override
void handlePlatformUndo(String direction) {
Copy link
Contributor

Choose a reason for hiding this comment

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

In general I've been thinking about moving away from the pattern of passing raw, privately defined strings around from these platform methods.

Is there a way you could convert this direction to an enum or something that is publicly defined? Then in your tests you could also reference that instead of hardcoded "undo" and "redo" strings that I saw in a few places.

@@ -46,6 +47,68 @@ Offset textOffsetToPosition(WidgetTester tester, int offset) {
return endpoints[0].point + const Offset(0.0, -2.0);
}

Future<void> sendKeys(
Copy link
Contributor

Choose a reason for hiding this comment

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

Another nit here: I think that should actually be a doc comment with 3 slashes ///.

@@ -46,6 +47,68 @@ Offset textOffsetToPosition(WidgetTester tester, int offset) {
return endpoints[0].point + const Offset(0.0, -2.0);
}

Future<void> sendKeys(
Copy link
Contributor

Choose a reason for hiding this comment

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

By the way, I'm glad sendKeys will be reusable now. I've wanted it in other test files before.

Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

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

left a reply with a suggestion, otherwise this LGTM

/// If the state would still be the same before and after the undo/redo, this
/// will not be called. For example, receiving a redo when there is nothing
/// to redo will not call this method.
final void Function(T value) onTriggered;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this may be a corner case that is not so common in real life. I vote for just assert the value should stay the same after this method is called. If this does crash some use case, we can figure out how to handle it later. Also at that point we may have a better picture on how we should support this use case

@justinmc
Copy link
Contributor

justinmc commented Jan 6, 2023

@fbcouch Can you update this branch by merging in master? GitHub is saying there is a conflict, though it doesn't say what the conflict is...

Otherwise I think we need to merge this!

@jmagman
Copy link
Member

jmagman commented Jan 6, 2023

Thanks for sticking with this for so long, @fbcouch!

@justinmc
Copy link
Contributor

justinmc commented Jan 6, 2023

I spoke a little too soon, we should make sure that this comment thread is resolved. I just left a comment explaining the composing text behavior.

@fbcouch
Copy link
Contributor Author

fbcouch commented Mar 7, 2023

@justinmc @chunhtai I merged main into this branch again, trying to preserve relevant changes to the old TextEditingHistory from #120889, #120062, and #119028

I think we should have everything wrapped up here (see: https://github.com/flutter/flutter/pull/98294/files#r1080613858 )

Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

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

Renewing my LGTM with the latest changes 👍

Thanks for doing all the work to bring this back up to date. I'll plan to merge it tomorrow morning if no one else objects.

/// If the state would still be the same before and after the undo/redo, this
/// will not be called. For example, receiving a redo when there is nothing
/// to redo will not call this method.
final void Function(T value) onTriggered;
Copy link
Contributor

Choose a reason for hiding this comment

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

I tried this out again and it seems to work for me on both iOS and Android with Gboard, where it correctly includes composing state in the undo history. I also tried the example above and got the assertion. Thanks!

@justinmc justinmc added the autosubmit Merge PR when tree becomes green via auto submit App label Mar 8, 2023
@auto-submit auto-submit bot merged commit 2a67bf7 into flutter:master Mar 8, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 9, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 10, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 10, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 10, 2023
hannah-hyj pushed a commit to hannah-hyj/flutter that referenced this pull request Mar 11, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 11, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 12, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request May 10, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request May 10, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a: tests "flutter test", flutter_test, or one of our tests a: text input Entering text in a text field or keyboard related problems autosubmit Merge PR when tree becomes green via auto submit App f: cupertino flutter/packages/flutter/cupertino repository f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants