Skip to content
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

Integrate IME for mobile doc editing #374

Merged
merged 62 commits into from
Feb 3, 2022
Merged

Conversation

matthew-carroll
Copy link
Contributor

Integrates IME input with document editing on Android and iOS.

The example app includes a demo for iOS and Android. Each one is styled appropriately, but regardless of which demo you run, the IME connection that it's using is based on the underlying platform. Therefore, even if the Android example is run on iOS, it's still using the standard iOS IME deltas.

Known issues:

  • There are timing issues with caret placement. The caret often appears in the wrong place.
  • The first time that the user taps a selection location in a document, the delete button doesn't work. This might be an Android bug, or it might be a Flutter bug. I don't think it's a super_editor bug.
  • On iOS, selecting an auto-correction from the iOS keyboard inserts the desired changes, but doesn't delete the old content that it's replacing. I think this is probably a Flutter bug.
  • The selection handles sit on top of the docked editing toolbar above the keyboard.
  • If the user selects content that appears beneath the keyboard, there's no auto-scrolling.

@matthew-carroll
Copy link
Contributor Author

@miguelcmedeiros @Jethro87 - Can each of you try out this PR for mobile document editing?

I'd like to figure out the minimal set of changes/fixes from this point to where each of you can ship your desired features.

@Jethro87
Copy link
Contributor

@matthew-carroll I've run Mobile Editing - Android and Mobile Editing - iOS editor configs on the iOS simulator, my physical iOS device, and a physical Android device, and I'm unable to make the keyboard appear (thus unable to type).

On the iOS simulator, selecting 'Toggle Software Keyboard' results in this output:

flutter: INFO: 2021-12-30 13:10:49.521762: Handling key press: RawKeyDownEvent#28a2e(logicalKey: LogicalKeyboardKey#0006a(keyId: "0x0000006a", keyLabel: "J", debugName: "Key J"), physicalKey: PhysicalKeyboardKey#7000d(usbHidUsage: "0x0007000d", debugName: "Key J"))
flutter: [document_keyboard_actions.dart] - deleteExpandedSelectionWhenCharacterOrDestructiveKeyPressed: Running...
flutter: FINER: 2021-12-30 13:10:49.616007: Received key event, but ignoring because it's not a down event: RawKeyUpEvent#6eb6d(logicalKey: LogicalKeyboardKey#0006a(keyId: "0x0000006a", keyLabel: "J", debugName: "Key J"), physicalKey: PhysicalKeyboardKey#7000d(usbHidUsage: "0x0007000d", debugName: "Key J"))

I get the same output when I type any letter using my physical keyboard in the iOS simulator:

flutter: INFO: 2021-12-30 13:16:15.770037: Handling key press: RawKeyDownEvent#e39a6(logicalKey: LogicalKeyboardKey#00064(keyId: "0x00000064", keyLabel: "D", debugName: "Key D"), physicalKey: PhysicalKeyboardKey#70007(usbHidUsage: "0x00070007", debugName: "Key D"))
flutter: [document_keyboard_actions.dart] - deleteExpandedSelectionWhenCharacterOrDestructiveKeyPressed: Running...
flutter: FINER: 2021-12-30 13:16:15.903071: Received key event, but ignoring because it's not a down event: RawKeyUpEvent#3645f(logicalKey: LogicalKeyboardKey#00064(keyId: "0x00000064", keyLabel: "D", debugName: "Key D"), physicalKey: PhysicalKeyboardKey#70007(usbHidUsage: "0x00070007", debugName: "Key D"))

Can you point me in the right direction here?

@osaxma
Copy link
Contributor

osaxma commented Dec 30, 2021

In Mobile Editing - iOS, when inserting a new line, I'm getting the exceptions below for the following cases:

  • When the line is inserted at the end of a paragraph, the newline is not inserted and the second attempt hides the keyboard.
  • When inserting the line in the middle of a paragraph, the newline is inserted yet the exceptions still appear.
click to expand
════════ Exception caught by foundation library ════════════════════════════════
The following _CastError was thrown while dispatching notifications for DocumentComposer:
Null check operator used on a null value

When the exception was thrown, this was the stack
#0      _IOSDocumentTouchInteractorState._positionCaret
package:super_editor/…/default_editor/document_gestures_touch_ios.dart:707
#1      _IOSDocumentTouchInteractorState._onSelectionChange
package:super_editor/…/default_editor/document_gestures_touch_ios.dart:249
#2      ChangeNotifier.notifyListeners
package:flutter/…/foundation/change_notifier.dart:308
#3      new DocumentComposer.<anonymous closure>
package:super_editor/…/core/document_composer.dart:20
#4      ChangeNotifier.notifyListeners
package:flutter/…/foundation/change_notifier.dart:308
#5      ComposerPreferences.clearStyles
package:super_editor/…/core/document_composer.dart:120
#6      _SuperEditorState._updateComposerPreferencesAtSelection
package:super_editor/…/default_editor/super_editor.dart:305
#7      ChangeNotifier.notifyListeners
package:flutter/…/foundation/change_notifier.dart:308
#8      DocumentComposer.selection=
package:super_editor/…/core/document_composer.dart:41
#9      CommonEditorOperations.insertBlockLevelNewline
package:super_editor/…/default_editor/common_editor_operations.dart:1592
#10     _DocumentImeInteractorState.updateEditingValueWithDeltas
package:super_editor/…/default_editor/document_input_ime.dart:215
#11     TextInput._handleTextInputInvocation
package:flutter/…/services/text_input.dart:1557
#12     MethodChannel._handleAsMethodCall
package:flutter/…/services/platform_channel.dart:404
#13     MethodChannel.setMethodCallHandler.<anonymous closure>
package:flutter/…/services/platform_channel.dart:397
#14     _DefaultBinaryMessenger.setMessageHandler.<anonymous closure>
package:flutter/…/services/binding.dart:389
#15     _DefaultBinaryMessenger.setMessageHandler.<anonymous closure>
package:flutter/…/services/binding.dart:386
#16     _invoke2.<anonymous closure> (dart:ui/hooks.dart:189:15)
#20     _invoke2 (dart:ui/hooks.dart:188:10)
#21     _ChannelCallbackRecord.invoke (dart:ui/channel_buffers.dart:42:5)
#22     _Channel.push (dart:ui/channel_buffers.dart:132:31)
#23     ChannelBuffers.push (dart:ui/channel_buffers.dart:329:17)
#24     PlatformDispatcher._dispatchPlatformMessage (dart:ui/platform_dispatcher.dart:544:22)
#25     _dispatchPlatformMessage (dart:ui/hooks.dart:83:31)
(elided 3 frames from dart:async)
The DocumentComposer sending notification was: Instance of 'DocumentComposer'
════════════════════════════════════════════════════════════════════════════════

════════ Exception caught by foundation library ════════════════════════════════
Null check operator used on a null value
════════════════════════════════════════════════════════════════════════════════

@osaxma
Copy link
Contributor

osaxma commented Dec 30, 2021

On iOS, selecting an auto-correction from the iOS keyboard inserts the desired changes, but doesn't delete the old content that it's replacing. I think this is probably a Flutter bug.

Btw, this is working fine on my end (except for the caret placement after the replacement -- I guess that's related to the first issue). I'm using Flutter 2.8.1 stable with iPhone 13 (iOS 15) simulator.

Another note, I believe the selection controls should disappear after any type of insertion or deletion (auto correct or typing).


Edit:
Additional note:
While this may not be a priority, Voice Dictation and Swipe to Text (aka QuickPath) are working fine when used within a paragraph but they don't work on an empty paragraph (they end up hiding the keyboard without anything being inserted).

Everything else seems to work great for iOS.

@matthew-carroll
Copy link
Contributor Author

@Jethro87 are you running the example app, or trying to use it in your own app? If you're using the example app, did you open the mobile-specific examples?

@matthew-carroll
Copy link
Contributor Author

@osaxma for the newline, are you just pressing the action button on the keyboard to get the newline, or are you doing something else?

@osaxma
Copy link
Contributor

osaxma commented Dec 30, 2021

I am using the action button on the virtual keyboard. It looks like the error happens because _docLayout.getRectForPosition method is returning null for some reason:

Screen Shot 2021-12-31 at 1 57 06 AM

Here's the log:
Restarted application in 457ms.
flutter: Initializing logger: gestures
flutter: Initializing logger: ime
flutter: INFO: 2021-12-31 02:05:50.327720: Attaching TextInputClient to TextInput
flutter: FINE: 2021-12-31 02:05:50.336423: Is attached to input client? true
flutter: INFO: 2021-12-31 02:05:51.739255: Tap down on document
flutter: FINE: 2021-12-31 02:05:51.740607:  - document offset: Offset(286.0, 442.0)
flutter: FINE: 2021-12-31 02:05:51.746423:  - tapped document position: [DocumentPosition] - node: "983fe021-a8ec-4053-b7fc-3e7ae3c95881", position: (TextPosition(offset: 439, affinity: TextAffinity.upstream))
flutter: FINE: 2021-12-31 02:05:51.747702: Setting document selection to [DocumentPosition] - node: "983fe021-a8ec-4053-b7fc-3e7ae3c95881", position: (TextPosition(offset: 439, affinity: TextAffinity.upstream))
flutter: Composer setting new selection
flutter: INFO: 2021-12-31 02:05:51.749230: Document composer (522470534) changed. New selection: [DocumentSelection] -
  base: ([DocumentPosition] - node: "983fe021-a8ec-4053-b7fc-3e7ae3c95881", position: (TextPosition(offset: 439, affinity: TextAffinity.upstream))),
  extent: ([DocumentPosition] - node: "983fe021-a8ec-4053-b7fc-3e7ae3c95881", position: (TextPosition(offset: 439, affinity: TextAffinity.upstream)))
flutter: Syncing IME with Doc and Composer
flutter: Doc node 983fe021-a8ec-4053-b7fc-3e7ae3c95881 has IME range: TextRange(start: 0, end: 439)
flutter: Doc node 983fe021-a8ec-4053-b7fc-3e7ae3c95881 has IME range: TextRange(start: 0, end: 439)
flutter: New text editing value: TextEditingValue(text: ┤Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus sed sagittis urna. Aenean mattis ante justo, quis sollicitudin metus interdum id. Aenean ornare urna ac enim consequat mollis. In aliquet convallis efficitur. Phasellus convallis purus in fringilla scelerisque. Ut ac orci a turpis egestas lobortis. Morbi aliquam dapibus sem, vitae sodales arcu ultrices eu. Duis vulputate mauris quam, eleifend pulvinar quam blandit eget.├, selection: TextSelection.collapsed(offset: 439, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: Existing text editing value: TextEditingValue(text: ┤├, selection: TextSelection.invalid, composing: TextRange(start: -1, end: -1))
flutter: INFO: 2021-12-31 02:05:51.755871: Sending new text editing value to OS: TextEditingValue(text: ┤Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus sed sagittis urna. Aenean mattis ante justo, quis sollicitudin metus interdum id. Aenean ornare urna ac enim consequat mollis. In aliquet convallis efficitur. Phasellus convallis purus in fringilla scelerisque. Ut ac orci a turpis egestas lobortis. Morbi aliquam dapibus sem, vitae sodales arcu ultrices eu. Duis vulputate mauris quam, eleifend pulvinar quam blandit eget.├, selection: TextSelection.collapsed(offset: 439, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: Composer preferences changed
flutter: Composer preferences changed
flutter: INFO: 2021-12-31 02:05:51.767736: Gained focus
flutter: INFO: 2021-12-31 02:05:53.332140: Received edit deltas from platform
flutter: INFO: 2021-12-31 02:05:53.332453: Instance of 'TextEditingDeltaInsertion'
flutter: INFO: 2021-12-31 02:05:53.332764: Applying delta: Instance of 'TextEditingDeltaInsertion'
flutter: FINE: 2021-12-31 02:05:53.333187: Inserting text:
flutter: FINE: 2021-12-31 02:05:53.334565: Creating doc selection from IME selection: TextSelection.collapsed(offset: 439, affinity: TextAffinity.downstream, isDirectional: false)
flutter: FINE: 2021-12-31 02:05:53.334927: Returning doc selection without modification
flutter: Composer setting new selection
flutter: INFO: 2021-12-31 02:05:53.336342: Document composer (522470534) changed. New selection: [DocumentSelection] -
  base: ([DocumentPosition] - node: "983fe021-a8ec-4053-b7fc-3e7ae3c95881", position: (TextPosition(offset: 439, affinity: TextAffinity.downstream))),
  extent: ([DocumentPosition] - node: "983fe021-a8ec-4053-b7fc-3e7ae3c95881", position: (TextPosition(offset: 439, affinity: TextAffinity.downstream)))
flutter: Syncing IME with Doc and Composer
flutter: Doc node 983fe021-a8ec-4053-b7fc-3e7ae3c95881 has IME range: TextRange(start: 0, end: 439)
flutter: Doc node 983fe021-a8ec-4053-b7fc-3e7ae3c95881 has IME range: TextRange(start: 0, end: 439)
flutter: New text editing value: TextEditingValue(text: ┤Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus sed sagittis urna. Aenean mattis ante justo, quis sollicitudin metus interdum id. Aenean ornare urna ac enim consequat mollis. In aliquet convallis efficitur. Phasellus convallis purus in fringilla scelerisque. Ut ac orci a turpis egestas lobortis. Morbi aliquam dapibus sem, vitae sodales arcu ultrices eu. Duis vulputate mauris quam, eleifend pulvinar quam blandit eget.├, selection: TextSelection.collapsed(offset: 439, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: Existing text editing value: TextEditingValue(text: ┤Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus sed sagittis urna. Aenean mattis ante justo, quis sollicitudin metus interdum id. Aenean ornare urna ac enim consequat mollis. In aliquet convallis efficitur. Phasellus convallis purus in fringilla scelerisque. Ut ac orci a turpis egestas lobortis. Morbi aliquam dapibus sem, vitae sodales arcu ultrices eu. Duis vulputate mauris quam, eleifend pulvinar quam blandit eget.├, selection: TextSelection.collapsed(offset: 439, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: Composer preferences changed
flutter: Composer preferences changed
flutter: Composer setting new selection
flutter: INFO: 2021-12-31 02:05:53.345944: Document composer (522470534) changed. New selection: [DocumentSelection] -
  base: ([DocumentPosition] - node: "67edc70e-47f0-4617-a6df-fa6e1587af0b", position: (TextPosition(offset: 0, affinity: TextAffinity.downstream))),
  extent: ([DocumentPosition] - node: "67edc70e-47f0-4617-a6df-fa6e1587af0b", position: (TextPosition(offset: 0, affinity: TextAffinity.downstream)))
flutter: Syncing IME with Doc and Composer
flutter: FINE: 2021-12-31 02:05:53.346408: Prepending upstream character for IME
flutter: Doc node 67edc70e-47f0-4617-a6df-fa6e1587af0b has IME range: TextRange(start: 1, end: 1)
flutter: Doc node 67edc70e-47f0-4617-a6df-fa6e1587af0b has IME range: TextRange(start: 1, end: 1)
flutter: New text editing value: TextEditingValue(text: ┤*├, selection: TextSelection.collapsed(offset: 1, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: Existing text editing value: TextEditingValue(text: ┤Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus sed sagittis urna. Aenean mattis ante justo, quis sollicitudin metus interdum id. Aenean ornare urna ac enim consequat mollis. In aliquet convallis efficitur. Phasellus convallis purus in fringilla scelerisque. Ut ac orci a turpis egestas lobortis. Morbi aliquam dapibus sem, vitae sodales arcu ultrices eu. Duis vulputate mauris quam, eleifend pulvinar quam blandit eget.├, selection: TextSelection.collapsed(offset: 439, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: INFO: 2021-12-31 02:05:53.347354: Sending new text editing value to OS: TextEditingValue(text: ┤*├, selection: TextSelection.collapsed(offset: 1, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: Composer preferences changed

════════ Exception caught by foundation library ════════════════════════════════
The following _CastError was thrown while dispatching notifications for DocumentComposer:
Null check operator used on a null value

When the exception was thrown, this was the stack
#0      _IOSDocumentTouchInteractorState._positionCaret
#1      _IOSDocumentTouchInteractorState._onSelectionChange
#2      ChangeNotifier.notifyListeners
#3      new DocumentComposer.<anonymous closure>
#4      ChangeNotifier.notifyListeners
#5      ComposerPreferences.clearStyles
#6      _SuperEditorState._updateComposerPreferencesAtSelection
#7      ChangeNotifier.notifyListeners
#8      DocumentComposer.selection=
#9      CommonEditorOperations.insertBlockLevelNewline
#10     _DocumentImeInteractorState.updateEditingValueWithDeltas
#11     TextInput._handleTextInputInvocation
#12     MethodChannel._handleAsMethodCall
#13     MethodChannel.setMethodCallHandler.<anonymous closure>
#14     _DefaultBinaryMessenger.setMessageHandler.<anonymous closure>
#15     _DefaultBinaryMessenger.setMessageHandler.<anonymous closure>
#16     _invoke2.<anonymous closure> (dart:ui/hooks.dart:189:15)
#20     _invoke2 (dart:ui/hooks.dart:188:10)
#21     _ChannelCallbackRecord.invoke (dart:ui/channel_buffers.dart:42:5)
#22     _Channel.push (dart:ui/channel_buffers.dart:132:31)
#23     ChannelBuffers.push (dart:ui/channel_buffers.dart:329:17)
#24     PlatformDispatcher._dispatchPlatformMessage (dart:ui/platform_dispatcher.dart:544:22)
#25     _dispatchPlatformMessage (dart:ui/hooks.dart:83:31)
(elided 3 frames from dart:async)
The DocumentComposer sending notification was: Instance of 'DocumentComposer'
════════════════════════════════════════════════════════════════════════════════

════════ Exception caught by foundation library ════════════════════════════════
Null check operator used on a null value
════════════════════════════════════════════════════════════════════════════════

@Jethro87
Copy link
Contributor

@Jethro87 are you running the example app, or trying to use it in your own app? If you're using the example app, did you open the mobile-specific examples?

@matthew-carroll I'm running the example app. super_editor --> example --> main.dart, then I'm toggling Mobile Editing - iOS and Mobile Editing - Android in Editor Configs in the Drawer.

I was originally running in Flutter master, then switched to stable with the same issue.

@matthew-carroll
Copy link
Contributor Author

matthew-carroll commented Dec 31, 2021

@Jethro87 what happens when you tap around the document? Does the caret and/or handles appear? Does the keyboard come up when you place the caret?

Also, I'm a bit confused by the output you shared above. It looks like you're trying to use the keyboard input, not the IME input. You're sure that you opened one of the mobile demos? There shouldn't even be a keyboard listener in those...

Is it possible that you're using the latest version of super_editor on master, rather than this PR branch? Those mobile examples already exist on master but they don't have any of the IME input that's implemented in this PR.

@Jethro87
Copy link
Contributor

Is it possible that you're using the latest version of super_editor on master, rather than this PR branch? Those mobile examples already exist on master but they don't have any of the IME input that's implemented in this PR.

That's it, @matthew-carroll, thanks. I initially was going to run this in my own app, as I had things set up from the last time. After switching to running the example, I neglected to check out the proper branch. Working now. Will test it thoroughly tomorrow.

@Jethro87
Copy link
Contributor

The following two points look to be the same as above #374 (comment)

  • Tapping return to create a new line mid-paragraph splits the paragraph, as expected, but the carat does not move. Tapping return again closes the keyboard instead of creating another new line. Here's the output when tapping to create a new line:
flutter: Composer preferences changed
flutter: Another exception was thrown: Instance of 'ErrorSummary'
flutter: Another exception was thrown: Instance of 'ErrorSummary'
  • Tapping return to create a new line at the end of a paragraph creates a new line (carat does not move as per the above point), but tapping into the new, blank line is very difficult. I have to close the keyboard before I'm able to tap into the new line. When I'm finally able to get the cursor into the new line, tapping any character closes the keyboard and does not insert the character.

The following points are for iOS.

  • Holding the spacebar on iOS should (I believe) bring up the magnifier as tapping and holding on the document does, and allow the user to move the carat over the document.

  • When a piece of content is selected, the toolbar options become unselectable. This means that I am unable to, for example, bold several words within a paragraph. I understand inline formatting options are not yet available in the toolbar, so perhaps this is why this is happening right now.

  • I don't seem to be able to delete the image at the top of the document, by attempting to select it or by backspacing from the title.

  • Selecting content across nodes (paragraph and list item, for example) and attempting to delete sometimes does nothing. This seems to reliably occur when selecting the first paragraph, the following blockquote, and a list item. Here's the output when selecting and tapping delete (nothing ends up being deleted):

flutter: Composer preferences changed
flutter: INFO: 2021-12-31 11:52:01.515878: Received edit deltas from platform
flutter: INFO: 2021-12-31 11:52:01.516227: Instance of 'TextEditingDeltaDeletion'
flutter: INFO: 2021-12-31 11:52:01.516326: Applying delta: Instance of 'TextEditingDeltaDeletion'
flutter: FINE: 2021-12-31 11:52:01.516523: Deleting text: quam blandit eget.
This is a blockquote!
This is an unordered list item
flutter: FINE: 2021-12-31 11:52:01.516638: Deleted range: TextRange(start: 412, end: 483)
flutter: FINE: 2021-12-31 11:52:01.516775: Creating doc selection from IME selection: TextSelection(baseOffset: 412, extentOffset: 483, isDirectional: false)
flutter: FINE: 2021-12-31 11:52:01.516962: Returning doc selection without modification
  • Tapping return in a list item creates a new list item with the correct content (if applicable), but the carat stays in the original list item. It should be moved to the new list item.

  • When changing a paragraph node to a header node (and vice versa), the carat doesn't resize.

  • When deleting an empty list item (with a list item above it), the carat does not move to the beginning of the empty line. It stays aligned with the other list items, rather than moving to the beginning of the empty line.

  • With the carat in a list item, tapping H1 or H2 does nothing, but the other formatting options work as expected (ordered/unordered list item, blockquote).

@miguelcmedeiros
Copy link
Collaborator

I also noticed that when we change node type using the keyboard toolbar, that it leaves the caret in the same coordinates with the same size. See the following video:
https://user-images.githubusercontent.com/31278849/147961565-dc486a93-ca6b-4d98-a3be-55227b2ac808.MP4

@matthew-carroll
Copy link
Contributor Author

@Jethro87 @osaxma @miguelcmedeiros - the caret timing issue should be resolved with the latest push

@miguelcmedeiros
Copy link
Collaborator

@Jethro87 @osaxma @miguelcmedeiros - the caret timing issue should be resolved with the latest push

I can confirm :)

@Jethro87
Copy link
Contributor

Jethro87 commented Jan 4, 2022

The following points are for Android.

  • When double-tapping on a word, the formatting toolbar appears (Cut/Copy/Paste/Select All), as expected. However, when I close the keyboard, the formatting toolbar is never removed, despite the selection and handles disappearing. This toolbar should be removed when the keyboard is closed.

  • When the single selection handle is visible (for a few seconds after tapping a word), if you format the paragraph to H1, for example, the selection handle doesn't move to the proper placement when the text size changes. Here's what this looks like:

Screenshot_20220104-094422

  • If you press return in the middle of a paragraph (thus it is split into two paragraphs with a space in between them), then format one of the paragraphs, both will be formatted. I would assume only one should be formatted. Example:

Screenshot_20220104-095301

@matthew-carroll
Copy link
Contributor Author

@Jethro87 can you describe the difficulty with selecting an empty paragraph? I know there was a caret timing issue, and there was an issue with inserting new characters, both of which should now be fixed. But you also mentioned that you had a hard time selecting the empty paragraph. I just tried tapping anywhere horizontally within an empty paragraph and it's bringing the keyboard up for me without issue.

@Jethro87
Copy link
Contributor

Jethro87 commented Feb 1, 2022

@matthew-carroll I found a few more issues.

  • Underline doesn't do anything (not sure if this is on purpose right now)

  • Placing the carat in a word then double spacing inserts a period.

super_editor_double_space_period.mov
  • Typing a word incorrectly followed by pressing the spacebar inserts the autocorrected word after the incorrect word. It should replace the incorrect word.
super_editor_autocorrect_bug.mov
  • Highlighting a word within a line or paragraph disables the block formatting options (headers). The correct behavior should show all formatting options (inline and block level) as tappable, but if a block format is selected, the entire block is formatted.
super_editor_block_formatting_selection.mov
  • When there hasn't been a selection, but the carat is simply at the beginning of a node or within a word, tapping the inline formatting options enables the functionality (as expected), but the toolbar images don't show that they are selected. Tapping on an inline formatting option should highlight the option in the toolbar and apply the formatting when typing begins.
super_editor_inline_formatting_not_showing.mov
  • Selecting multiple nodes and backspacing the selection results in a grey screen and the following error message for every selected node. I can replicate this by selecting a list item node and a paragraph node, as well as a header node and a paragraph node.
flutter: Another exception was thrown: Instance of 'ErrorSummary'
flutter: INFO: 2022-02-01 09:16:27.785405: WARNING: found component but it's not a DocumentComponent: c125e090-5670-409d-880c-0a51a61d0272, layout key: [GlobalKey#a4edf], state: null, widget: null, context: null
flutter: INFO: 2022-02-01 09:16:27.785427: Could not find any component for node position: [DocumentPosition] - node: "c125e090-5670-409d-880c-0a51a61d0272", position: (TextPosition(offset: 0, affinity: TextAffinity.downstream))
super_editor_delete_selection_bug.mov
  • In a typical iOS text field, capitalization is automatically set at the beginning of a line, and >= 1 space after a period. This can be seen by the bolded shift key. Currently in super_editor, lower case is always set.

Copy link
Collaborator

@miguelcmedeiros miguelcmedeiros left a comment

Choose a reason for hiding this comment

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

I managed to integrate these changes into our project and most of it works well, with one exception.

Not sure if I'm misusing the API, but I don't seem to get the clipper passed to createOverlayControlsClipper to refresh, so that it can accommodate for showing/hiding the toolbar or changing its height.

}

// Place the caret at the beginning of the second node.
// Place the caret at the beginning of the new node.
print("Placing caret in new text node: $newNodeId");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remove print

return ListenableBuilder(
listenable: widget.editingController,
builder: (context) {
print("Clipper method: ${widget.createOverlayControlsClipper}");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remove print

@miguelcmedeiros
Copy link
Collaborator

Not sure if I'm misusing the API, but I don't seem to get the clipper passed to createOverlayControlsClipper to refresh, so that it can accommodate for showing/hiding the toolbar or changing its height.

@matthew-carroll
I tried passing createOverlayControlsClipper as a value instead of a builder, and that seemed to do the trick. Is there a need to have it as a builder?

@matthew-carroll
Copy link
Contributor Author

@miguelcmedeiros the builder provides access to a BuildContext in case the developer needs to lookup the dimensions of some other RenderBox to choose clipping bounds.

Passing a CustomClipper instead of a builder should cause a compilation error. I'm not sure how you did that.

If the demo configuration isn't working, please provide repro steps so that I can see what's not working on my end.

@matthew-carroll
Copy link
Contributor Author

@Jethro87 Is the auto-capitalization behavior a ship blocker for mobile editing?

I think we're going to have to introduce our own rules for that kind of behavior because we have to prepend a bogus character, like "~", before any text in a node. Without the bogus character, the keyboard won't report a deletion, which prevents us from deleting a paragraph and moving the caret up to the one above. But the inclusion of the bogus character makes the IME believe you're always editing the 2nd+ character.

Off the top of my head, I don't know what a quick solution would be to this problem.

@matthew-carroll
Copy link
Contributor Author

@miguelcmedeiros for the clipper, are you saying that you don't know how to re-calculate the clipper when your toolbar changes?

If so, try defining your own CustomClipper. In your own CustomClipper, take in some kind of Listenable that gets notified whenever your toolbar appears/disappears/changes height. Pass that Listenable into the super() constructor to cause a re-calculation whenever it notifies.

Something like:

class MyCustomClipper extends CustomClipper<Rect> {
  MyCustomClipper({
    required Listenable toolbarChangeNotifier,
  }) : super(reclip: toolbarChangeNotifier);
}

Then pass a builder that instantiates that clipper:

SuperEditor(
  //...
  createOverlayControlsClipper: (_) => MyCustomClipper(toolbarChangeNotifier: myToolbarWatcher),
);

@matthew-carroll
Copy link
Contributor Author

@Jethro87

  • Underline styling is implemented.
  • Auto-insertion period problem is resolved.
  • I wasn't able to repro the autocorrection issue. I think the period work may have solved that, too.
  • I changed the block-level formatting options to let you change it even when multiple characters are selected.
  • The mounted toolbar now highlights the active composing attributions.
  • The multi-node-deletion bug is fixed
  • For capitalization, I don't know what to do for the very first character, but I changed the policy to auto-capitalize for every sentence.

@miguelcmedeiros - I removed the print statements

@miguelcmedeiros
Copy link
Collaborator

@matthew-carroll, thanks for the tip with CustomClipper (reclip) 👍
That worked well.

@miguelcmedeiros
Copy link
Collaborator

@matthew-carroll, while testing using the latest changes we noticed that on Android pressing and holding the backspace key when the selection is at the end of a paragraph, it "releases" the key after deleting one character.
If the selection is somewhere in the middle of the paragraph and we press and hold the backspace key, then it will continue to delete character until we lift the finger.

@Jethro87
Copy link
Contributor

Jethro87 commented Feb 2, 2022

Thanks, @matthew-carroll, the other issues look good.

Auto-capitalization of the first word in a new node, as it is now, isn't a great experience. Not only does the user have to explicitly capitalize the first word (a non-standard behavior), but adding a new node causes the keyboard to "flash", as per this video:

super_editor_first_letter_capitalize.mov

Can you set the keyboard to (sentence) capitalize when the carat is next to the bogus character? If so, this might be a good enough workaround, and I wonder if this would get rid of the "flash" when a user adds a new node.

@matthew-carroll
Copy link
Contributor Author

@Jethro87 - I was able to get the desired capitalization behavior by replacing the invisible bogus characters, like "~" and "^" at the front of the text, with the substring ". ". This makes the IME think that the text at the beginning of an empty paragraph is actually the start of a new sentence, so you get caps.

I recommend doing a full regression test. I can't write automated tests around this behavior because it depends upon unpredictable delta reporting from the OS.

@miguelcmedeiros - I don't think I'm gonna be able to solve that deletion problem. The issue is that whenever we send a composing TextRange back to the OS, on Android, if that region doesn't match what the OS expects it to be, then Flutter restarts the whole input method.

When you hold down the delete button, Android sends us a non-zero composing region. We don't care about composing regions, so we always send TextRange.empty back to the OS. But when we do that, Flutter panics and restarts the input method, which causes the OS to forget the deletion process it was going through.

I tried to always send the OS whatever composing region it sends us, but that resulted in occasional TextRange validity errors, and even when there weren't any errors, the OS would still manage to end up in an inconsistent state, triggering that input method restart.

You can see the Flutter code that forces this restart, here:
https://github.com/flutter/engine/blob/main/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java#L448

and

https://github.com/flutter/engine/blob/main/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java#L474

Please let me know if there are any more ship blocking issues, or if we can merge this in.

@matthew-carroll matthew-carroll marked this pull request as ready for review February 3, 2022 00:53
@miguelcmedeiros
Copy link
Collaborator

I don't think I'm gonna be able to solve that deletion problem.

@matthew-carroll, this doesn't have to block this PR, but it has to be solved before we release Android apps with SuperEditor. I'll create a ticket for this issue, so that we can tackle it separately.

Copy link
Collaborator

@miguelcmedeiros miguelcmedeiros left a comment

Choose a reason for hiding this comment

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

LGTM

Didn't do an honest code review given the size/complexity of the PR, but with all the testing (demo app and superlist app), I don't see any blocker at the moment.

@Jethro87
Copy link
Contributor

Jethro87 commented Feb 3, 2022

@matthew-carroll The capitalization behavior is quite good, nice work. I did other testing and didn't find any issues, other than "Cut/Copy/Paste" still not working, but I think (?) this is intentional.

@matthew-carroll
Copy link
Contributor Author

@Jethro87 is there anything blocking you from implementing cut/copy/paste yourself?

I put that dummy popover toolbar in there just to prove that it can be placed and moved correctly. But given that Flutter still doesn't have a legitimate copy buffer, I think each app is gonna want to decide how to interpret that data. For example, when you copy the content, what format should we use? Markdown, HTML, something else?

@Jethro87
Copy link
Contributor

Jethro87 commented Feb 3, 2022

@matthew-carroll Right, that makes sense. This isn't a problem I've given thought to but I understand the issue. Will think more on it.

@matthew-carroll
Copy link
Contributor Author

Ok, I'm gonna merge this in. Fingers crossed.

@matthew-carroll matthew-carroll merged commit c486e18 into main Feb 3, 2022
@matthew-carroll matthew-carroll deleted the 360_integrate-ime branch February 3, 2022 04:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants