From 3297a1fb9a717eaeb21201dce371e4ff88a0a822 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Fri, 21 Feb 2025 01:13:18 -0400 Subject: [PATCH 01/40] Chore: improved input client service --- example/pubspec.lock | 86 +++--- lib/src/controller/quill_controller.dart | 2 + lib/src/delta/delta_diff.dart | 1 - lib/src/editor/input/debounce/debounce.dart | 40 +++ lib/src/editor/input/diff_services.dart | 68 +++++ lib/src/editor/input/ime/on_delete.dart | 32 ++ lib/src/editor/input/ime/on_insert.dart | 29 ++ .../editor/input/ime/on_non_update_text.dart | 31 ++ .../editor/input/ime/on_replace_method.dart | 85 ++++++ .../text_editor_input_client_mixin.dart} | 282 ++++++++++++------ .../editor_keyboard_shortcuts.dart | 13 - .../editor/raw_editor/raw_editor_state.dart | 5 +- 12 files changed, 527 insertions(+), 147 deletions(-) create mode 100644 lib/src/editor/input/debounce/debounce.dart create mode 100644 lib/src/editor/input/diff_services.dart create mode 100644 lib/src/editor/input/ime/on_delete.dart create mode 100644 lib/src/editor/input/ime/on_insert.dart create mode 100644 lib/src/editor/input/ime/on_non_update_text.dart create mode 100644 lib/src/editor/input/ime/on_replace_method.dart rename lib/src/editor/{raw_editor/raw_editor_state_text_input_client_mixin.dart => input/text_editor_input_client_mixin.dart} (57%) diff --git a/example/pubspec.lock b/example/pubspec.lock index 52f375c79..4c9b4098b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -13,26 +13,26 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.11.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" characters: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.3.0" charcode: dependency: transitive description: @@ -45,18 +45,18 @@ packages: dependency: transitive description: name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.1" collection: dependency: transitive description: name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.19.1" + version: "1.19.0" cross_file: dependency: transitive description: @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.1" ffi: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.0" file_selector_linux: dependency: transitive description: @@ -373,18 +373,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -413,10 +413,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: @@ -429,10 +429,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.15.0" mime: dependency: transitive description: @@ -445,10 +445,10 @@ packages: dependency: "direct main" description: name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.9.0" photo_view: dependency: transitive description: @@ -461,10 +461,10 @@ packages: dependency: transitive description: name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.6" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -477,10 +477,10 @@ packages: dependency: transitive description: name: process - sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" url: "https://pub.dev" source: hosted - version: "5.0.3" + version: "5.0.2" quill_native_bridge: dependency: transitive description: @@ -562,34 +562,34 @@ packages: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.10.1" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.12.1" + version: "1.12.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.2" string_scanner: dependency: transitive description: name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.3.0" sync_http: dependency: transitive description: @@ -602,18 +602,18 @@ packages: dependency: transitive description: name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.3" typed_data: dependency: transitive description: @@ -738,10 +738,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "14.3.0" web: dependency: transitive description: @@ -767,5 +767,5 @@ packages: source: hosted version: "5.10.1" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.6.0 <4.0.0" flutter: ">=3.27.0" diff --git a/lib/src/controller/quill_controller.dart b/lib/src/controller/quill_controller.dart index 0d2d291a4..8e9b991bf 100644 --- a/lib/src/controller/quill_controller.dart +++ b/lib/src/controller/quill_controller.dart @@ -278,6 +278,8 @@ class QuillController extends ChangeNotifier { return; } + print('Remove params: len: $len, index: $index'); + Delta? delta; Style? style; if (len > 0 || data is! String || data.isNotEmpty) { diff --git a/lib/src/delta/delta_diff.dart b/lib/src/delta/delta_diff.dart index acbe17d9a..e54dd8261 100644 --- a/lib/src/delta/delta_diff.dart +++ b/lib/src/delta/delta_diff.dart @@ -39,7 +39,6 @@ Diff getDiff(String oldText, String newText, int cursorPosition) { end > limit && oldText[end - 1] == newText[end + delta - 1]; end--) {} var start = 0; - //TODO: we need to improve this part because this loop has a lot of unsafe index operations for (final startLimit = cursorPosition - math.max(0, delta); start < startLimit && (start > oldText.length - 1 ? '' : oldText[start]) == diff --git a/lib/src/editor/input/debounce/debounce.dart b/lib/src/editor/input/debounce/debounce.dart new file mode 100644 index 000000000..0c905e1c8 --- /dev/null +++ b/lib/src/editor/input/debounce/debounce.dart @@ -0,0 +1,40 @@ +import 'dart:async'; +import 'dart:ui'; + +class Debounce { + static final Map _actions = {}; + + static void debounce( + String key, + Duration duration, + VoidCallback callback, + ) { + if (duration == Duration.zero) { + // Call immediately + callback(); + cancel(key); + } else { + cancel(key); + _actions[key] = Timer( + duration, + () { + callback(); + cancel(key); + }, + ); + } + } + + static void cancel(String key) { + _actions[key]?.cancel(); + _actions.remove(key); + } + + static void clear() { + _actions + ..forEach((key, timer) { + timer.cancel(); + }) + ..clear(); + } +} diff --git a/lib/src/editor/input/diff_services.dart b/lib/src/editor/input/diff_services.dart new file mode 100644 index 000000000..95e2c5555 --- /dev/null +++ b/lib/src/editor/input/diff_services.dart @@ -0,0 +1,68 @@ +import 'package:flutter/services.dart'; +import '../../delta/delta_diff.dart'; + +List getTextEditingDeltas( + TextEditingValue? oldValue, + TextEditingValue newValue, +) { + if (oldValue == null || oldValue.text == newValue.text) { + return [ + TextEditingDeltaNonTextUpdate( + oldText: newValue.text, + selection: newValue.selection, + composing: newValue.composing, + ), + ]; + } + final currentText = oldValue.text; + final diff = getDiff( + currentText, + newValue.text, + newValue.selection.extentOffset, + ); + if (diff.inserted.isNotEmpty && diff.deleted.isEmpty) { + return [ + TextEditingDeltaInsertion( + oldText: currentText, + textInserted: diff.inserted, + insertionOffset: diff.start, + selection: newValue.selection, + composing: newValue.composing, + ), + ]; + } else if (diff.inserted.isEmpty && diff.deleted.isNotEmpty) { + return [ + TextEditingDeltaDeletion( + oldText: currentText, + selection: newValue.selection, + composing: newValue.composing, + deletedRange: TextRange( + start: diff.start, + end: diff.start + diff.deleted.length, + ), + ), + ]; + } else if (diff.inserted.isNotEmpty && diff.deleted.isNotEmpty) { + return [ + TextEditingDeltaReplacement( + oldText: currentText, + selection: newValue.selection, + composing: newValue.composing, + replacementText: diff.inserted, + replacedRange: TextRange( + start: diff.start, + end: diff.start + diff.deleted.length, + ), + ), + ]; + } else if (diff.inserted.isEmpty && diff.deleted.isEmpty) { + return [ + TextEditingDeltaNonTextUpdate( + oldText: newValue.text, + selection: newValue.selection, + composing: newValue.composing, + ), + ]; + } + throw UnsupportedError('Unknown diff: $diff'); +} diff --git a/lib/src/editor/input/ime/on_delete.dart b/lib/src/editor/input/ime/on_delete.dart new file mode 100644 index 000000000..402779303 --- /dev/null +++ b/lib/src/editor/input/ime/on_delete.dart @@ -0,0 +1,32 @@ +import 'package:flutter/services.dart'; +import '../../../../../flutter_quill.dart'; + +Future onDelete( + TextEditingDeltaDeletion deletion, + QuillController controller, +) async { + final selection = controller.selection; + if (selection.isCollapsed) { + final start = deletion.deletedRange.start; + final length = deletion.deletedRange.end - start; + controller.replaceText( + start + 1, + length, + '', + TextSelection.collapsed( + offset: start > 0 ? start - 1 : 0, + affinity: controller.selection.affinity, + ), + ); + return; + } + controller.replaceText( + selection.baseOffset, + selection.extentOffset - selection.baseOffset, + '', + TextSelection.collapsed( + offset: selection.start, + affinity: selection.affinity, + ), + ); +} diff --git a/lib/src/editor/input/ime/on_insert.dart b/lib/src/editor/input/ime/on_insert.dart new file mode 100644 index 000000000..b5d0fe0f6 --- /dev/null +++ b/lib/src/editor/input/ime/on_insert.dart @@ -0,0 +1,29 @@ +import 'package:flutter/services.dart'; + +import '../../../controller/quill_controller.dart'; +import '../../raw_editor/config/events/character_shortcuts_events.dart'; + +Future onInsert( + TextEditingDeltaInsertion insertion, + QuillController controller, + List characterShortcutEvents, +) async { + final selection = controller.selection; + + final insertionText = insertion.textInserted; + + if (insertionText.length == 1 && !insertionText.contains('\n')) { + for (final shortcutEvent in characterShortcutEvents) { + if (shortcutEvent.character == insertionText && shortcutEvent.handler(controller)) { + return; + } + } + } + + controller.replaceText( + selection.baseOffset, + selection.extentOffset - selection.baseOffset, + insertionText, + TextSelection.collapsed(offset: selection.extentOffset + insertionText.length), + ); +} diff --git a/lib/src/editor/input/ime/on_non_update_text.dart b/lib/src/editor/input/ime/on_non_update_text.dart new file mode 100644 index 000000000..2d8b0455f --- /dev/null +++ b/lib/src/editor/input/ime/on_non_update_text.dart @@ -0,0 +1,31 @@ +import 'dart:io'; +import 'package:flutter/services.dart'; +import '../../../../../flutter_quill.dart'; + +Future onNonTextUpdate( + TextEditingDeltaNonTextUpdate nonTextUpdate, + QuillController controller, +) async { + // update the selection on Windows + // + // when typing characters with CJK IME on Windows, a non-text update is sent + // with the selection range. + + if (Platform.isWindows) { + if (nonTextUpdate.composing == TextRange.empty && nonTextUpdate.selection.isCollapsed) { + controller.updateSelection( + TextSelection.collapsed( + offset: nonTextUpdate.selection.start, + ), + ChangeSource.local, + ); + } + } else if (Platform.isLinux || Platform.isMacOS) { + controller.updateSelection( + TextSelection.collapsed( + offset: nonTextUpdate.selection.start, + ), + ChangeSource.local, + ); + } +} diff --git a/lib/src/editor/input/ime/on_replace_method.dart b/lib/src/editor/input/ime/on_replace_method.dart new file mode 100644 index 000000000..184f12507 --- /dev/null +++ b/lib/src/editor/input/ime/on_replace_method.dart @@ -0,0 +1,85 @@ +import 'dart:io'; + +import 'package:flutter/services.dart'; + +import '../../../controller/quill_controller.dart'; +import '../../raw_editor/config/events/character_shortcuts_events.dart'; +import 'on_insert.dart'; + +Future onReplace( + TextEditingDeltaReplacement replacement, + QuillController controller, + List characterShortcutEvents, +) async { + // delete the selection + final selection = controller.selection; + + final textReplacement = replacement.replacementText; + + if (selection.isCollapsed) { + if (textReplacement.length == 1) { + for (final shortcutEvent in characterShortcutEvents) { + if (shortcutEvent.character == textReplacement && shortcutEvent.handler(controller)) { + return; + } + } + } + + if (Platform.isIOS) { + // remove the trailing '\n' when pressing the return key + if (textReplacement.endsWith('\n')) { + replacement = TextEditingDeltaReplacement( + oldText: replacement.oldText, + replacementText: replacement.replacementText.substring(0, replacement.replacementText.length - 1), + replacedRange: replacement.replacedRange, + selection: replacement.selection, + composing: replacement.composing, + ); + } + } + + final start = replacement.replacedRange.start; + final length = replacement.replacedRange.end - start; + controller.replaceText( + start, + length, + textReplacement, + TextSelection.collapsed( + offset: replacement.selection.baseOffset + textReplacement.length + ), + ); + } else { + controller.replaceText( + selection.baseOffset, + selection.extentOffset - selection.baseOffset, + '', + TextSelection.collapsed( + offset: selection.baseOffset, + ), + ); + // insert the replacement + final insertion = replacement.toInsertion(); + await onInsert( + insertion, + controller, + characterShortcutEvents, + ); + } +} + +extension on TextEditingDeltaReplacement { + TextEditingDeltaInsertion toInsertion() { + final text = oldText.replaceRange( + replacedRange.start, + replacedRange.end, + '', + ); + return TextEditingDeltaInsertion( + oldText: text, + textInserted: replacementText, + insertionOffset: replacedRange.start, + selection: selection, + composing: composing, + ); + } +} diff --git a/lib/src/editor/raw_editor/raw_editor_state_text_input_client_mixin.dart b/lib/src/editor/input/text_editor_input_client_mixin.dart similarity index 57% rename from lib/src/editor/raw_editor/raw_editor_state_text_input_client_mixin.dart rename to lib/src/editor/input/text_editor_input_client_mixin.dart index dc9b0ba11..0ead0d778 100644 --- a/lib/src/editor/raw_editor/raw_editor_state_text_input_client_mixin.dart +++ b/lib/src/editor/input/text_editor_input_client_mixin.dart @@ -1,3 +1,5 @@ +import 'dart:io'; +import 'dart:math'; import 'dart:ui' show lerpDouble; import 'package:flutter/animation.dart' show Curves; @@ -6,14 +8,16 @@ import 'package:flutter/foundation.dart' show ValueNotifier, kIsWeb; import 'package:flutter/material.dart' show Theme; import 'package:flutter/scheduler.dart' show SchedulerBinding; import 'package:flutter/services.dart'; - -import '../../delta/delta_diff.dart'; import '../../document/document.dart'; -import '../editor.dart'; -import 'raw_editor.dart'; - -mixin RawEditorStateTextInputClientMixin on EditorState - implements TextInputClient { +import '../raw_editor/raw_editor.dart'; +import 'debounce/debounce.dart'; +import 'diff_services.dart'; +import 'ime/on_delete.dart'; +import 'ime/on_insert.dart'; +import 'ime/on_non_update_text.dart'; +import 'ime/on_replace_method.dart'; + +mixin TextEditorInputClientMixin on EditorState implements TextInputClient { TextInputConnection? _textInputConnection; TextEditingValue? __lastKnownRemoteTextEditingValue; @@ -24,8 +28,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState } } - TextEditingValue? get _lastKnownRemoteTextEditingValue => - __lastKnownRemoteTextEditingValue; + TextEditingValue? get _lastKnownRemoteTextEditingValue => __lastKnownRemoteTextEditingValue; /// The range of text that is currently being composed. final ValueNotifier composingRange = ValueNotifier( @@ -48,14 +51,12 @@ mixin RawEditorStateTextInputClientMixin on EditorState bool get shouldCreateInputConnection => kIsWeb || !widget.config.readOnly; /// Returns `true` if there is open input connection. - bool get hasConnection => - _textInputConnection != null && _textInputConnection!.attached; + bool get hasConnection => _textInputConnection != null && _textInputConnection!.attached; /// Opens or closes input connection based on the current state of /// [focusNode] and [value]. void openOrCloseConnection() { - if (widget.config.focusNode.hasFocus && - widget.config.focusNode.consumeKeyboardToken()) { + if (widget.config.focusNode.hasFocus && widget.config.focusNode.consumeKeyboardToken()) { openConnectionIfNeeded(); } else if (!widget.config.focusNode.hasFocus) { closeConnectionIfNeeded(); @@ -94,14 +95,10 @@ mixin RawEditorStateTextInputClientMixin on EditorState /// Trap selection extends off end of document if (_lastKnownRemoteTextEditingValue != null) { - if (_lastKnownRemoteTextEditingValue!.selection.end > - _lastKnownRemoteTextEditingValue!.text.length) { - _lastKnownRemoteTextEditingValue = _lastKnownRemoteTextEditingValue! - .copyWith( - selection: _lastKnownRemoteTextEditingValue!.selection - .copyWith( - extentOffset: - _lastKnownRemoteTextEditingValue!.text.length)); + if (_lastKnownRemoteTextEditingValue!.selection.end > _lastKnownRemoteTextEditingValue!.text.length) { + _lastKnownRemoteTextEditingValue = _lastKnownRemoteTextEditingValue!.copyWith( + selection: _lastKnownRemoteTextEditingValue!.selection + .copyWith(extentOffset: _lastKnownRemoteTextEditingValue!.text.length)); } } _textInputConnection!.setEditingState(_lastKnownRemoteTextEditingValue!); @@ -109,35 +106,29 @@ mixin RawEditorStateTextInputClientMixin on EditorState _textInputConnection!.show(); } + // windows void _updateComposingRectIfNeeded() { - final composingRange = _lastKnownRemoteTextEditingValue?.composing ?? - textEditingValue.composing; + final composingRange = _lastKnownRemoteTextEditingValue?.composing ?? textEditingValue.composing; if (hasConnection) { assert(mounted); if (composingRange.isValid) { final offset = composingRange.start; - final composingRect = - renderEditor.getLocalRectForCaret(TextPosition(offset: offset)); + final composingRect = renderEditor.getLocalRectForCaret(TextPosition(offset: offset)); _textInputConnection!.setComposingRect(composingRect); } - SchedulerBinding.instance - .addPostFrameCallback((_) => _updateComposingRectIfNeeded()); + //SchedulerBinding.instance.addPostFrameCallback((_) => _updateComposingRectIfNeeded()); } } + // macos void _updateCaretRectIfNeeded() { if (hasConnection) { - if (!dirty && - renderEditor.selection.isValid && - renderEditor.selection.isCollapsed) { - final currentTextPosition = - TextPosition(offset: renderEditor.selection.baseOffset); - final caretRect = - renderEditor.getLocalRectForCaret(currentTextPosition); + if (!dirty && renderEditor.selection.isValid && renderEditor.selection.isCollapsed) { + final currentTextPosition = TextPosition(offset: renderEditor.selection.baseOffset); + final caretRect = renderEditor.getLocalRectForCaret(currentTextPosition); _textInputConnection!.setCaretRect(caretRect); } - SchedulerBinding.instance - .addPostFrameCallback((_) => _updateCaretRectIfNeeded()); + //SchedulerBinding.instance.addPostFrameCallback((_) => _updateCaretRectIfNeeded()); } } @@ -188,8 +179,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState // Start TextInputClient implementation @override - TextEditingValue? get currentTextEditingValue => - _lastKnownRemoteTextEditingValue; + TextEditingValue? get currentTextEditingValue => _lastKnownRemoteTextEditingValue; // autofill is not needed @override @@ -218,21 +208,69 @@ mixin RawEditorStateTextInputClientMixin on EditorState return; } - final effectiveLastKnownValue = _lastKnownRemoteTextEditingValue!; + final deltas = getTextEditingDeltas(currentTextEditingValue, value); _lastKnownRemoteTextEditingValue = value; - final oldText = effectiveLastKnownValue.text; - final text = value.text; - final cursorPosition = value.selection.extentOffset; - final diff = getDiff(oldText, text, cursorPosition); - if (diff.deleted.isEmpty && diff.inserted.isEmpty) { - widget.controller.updateSelection(value.selection, ChangeSource.local); + // On mobile, the IME will send a lot of updateEditingValue events, so we + // need to debounce it to combine them together. + Debounce.debounce( + 'input', + Platform.isAndroid || Platform.isIOS + ? const Duration( + milliseconds: 10, + ) + : Duration.zero, + () { + _apply(deltas); + }, + ); + } + + Future _apply(List deltas) async { + final formattedDeltas = deltas.map((e) => e.format()).toList(); + for (final delta in formattedDeltas) { + _updateComposing(delta); + + if (delta is TextEditingDeltaInsertion) { + await onInsert( + delta, + widget.controller, + widget.config.characterShortcutEvents, + ); + } else if (delta is TextEditingDeltaDeletion) { + await onDelete( + delta, + widget.controller, + ); + } else if (delta is TextEditingDeltaReplacement) { + await onReplace( + delta, + widget.controller, + widget.config.characterShortcutEvents, + ); + } else if (delta is TextEditingDeltaNonTextUpdate) { + await onNonTextUpdate( + delta, + widget.controller, + ); + } + } + } + + void _updateComposing(TextEditingDelta delta) { + if (delta is TextEditingDeltaNonTextUpdate) { + composingRange.value = delta.composing; } else { - widget.controller.replaceText( - diff.start, - diff.deleted.length, - diff.inserted, - value.selection, - ); + composingRange.value = composingRange.value.start != -1 && delta.composing.end != -1 + ? TextRange( + start: composingRange.value.start, + end: delta.composing.end, + ) + : delta.composing; + } + + // solve the issue where the Chinese IME doesn't continue deleting after the input content has been deleted. + if (Platform.isMacOS && (composingRange.value.isCollapsed)) { + composingRange.value = TextRange.empty; } } @@ -281,49 +319,39 @@ mixin RawEditorStateTextInputClientMixin on EditorState // we cache the position. _pointOffsetOrigin = point.offset; - final currentTextPosition = - TextPosition(offset: renderEditor.selection.baseOffset); - _startCaretRect = - renderEditor.getLocalRectForCaret(currentTextPosition); + final currentTextPosition = TextPosition(offset: renderEditor.selection.baseOffset); + _startCaretRect = renderEditor.getLocalRectForCaret(currentTextPosition); - _lastBoundedOffset = _startCaretRect!.center - - _floatingCursorOffset(currentTextPosition); + _lastBoundedOffset = _startCaretRect!.center - _floatingCursorOffset(currentTextPosition); _lastTextPosition = currentTextPosition; - renderEditor.setFloatingCursor( - point.state, _lastBoundedOffset!, _lastTextPosition!); + renderEditor.setFloatingCursor(point.state, _lastBoundedOffset!, _lastTextPosition!); break; case FloatingCursorDragState.Update: assert(_lastTextPosition != null, 'Last text position was not set'); final floatingCursorOffset = _floatingCursorOffset(_lastTextPosition!); final centeredPoint = point.offset! - _pointOffsetOrigin!; - final rawCursorOffset = - _startCaretRect!.center + centeredPoint - floatingCursorOffset; + final rawCursorOffset = _startCaretRect!.center + centeredPoint - floatingCursorOffset; - final preferredLineHeight = - renderEditor.preferredLineHeight(_lastTextPosition!); + final preferredLineHeight = renderEditor.preferredLineHeight(_lastTextPosition!); _lastBoundedOffset = renderEditor.calculateBoundedFloatingCursorOffset( rawCursorOffset, preferredLineHeight, ); - _lastTextPosition = renderEditor.getPositionForOffset(renderEditor - .localToGlobal(_lastBoundedOffset! + floatingCursorOffset)); - renderEditor.setFloatingCursor( - point.state, _lastBoundedOffset!, _lastTextPosition!); - final newSelection = TextSelection.collapsed( - offset: _lastTextPosition!.offset, - affinity: _lastTextPosition!.affinity); + _lastTextPosition = renderEditor + .getPositionForOffset(renderEditor.localToGlobal(_lastBoundedOffset! + floatingCursorOffset)); + renderEditor.setFloatingCursor(point.state, _lastBoundedOffset!, _lastTextPosition!); + final newSelection = + TextSelection.collapsed(offset: _lastTextPosition!.offset, affinity: _lastTextPosition!.affinity); // Setting selection as floating cursor moves will have scroll view // bring background cursor into view - renderEditor.onSelectionChanged( - newSelection, SelectionChangedCause.forcePress); + renderEditor.onSelectionChanged(newSelection, SelectionChangedCause.forcePress); break; case FloatingCursorDragState.End: // We skip animation if no update has happened. if (_lastTextPosition != null && _lastBoundedOffset != null) { floatingCursorResetController ..value = 0.0 - ..animateTo(1, - duration: _floatingCursorResetTime, curve: Curves.decelerate); + ..animateTo(1, duration: _floatingCursorResetTime, curve: Curves.decelerate); } break; } @@ -336,25 +364,20 @@ mixin RawEditorStateTextInputClientMixin on EditorState /// and repositioned (linear interpolation between position of floating cursor /// and current position of background cursor) void onFloatingCursorResetTick() { - final finalPosition = - renderEditor.getLocalRectForCaret(_lastTextPosition!).centerLeft - - _floatingCursorOffset(_lastTextPosition!); + final finalPosition = renderEditor.getLocalRectForCaret(_lastTextPosition!).centerLeft - + _floatingCursorOffset(_lastTextPosition!); if (floatingCursorResetController.isCompleted) { - renderEditor.setFloatingCursor( - FloatingCursorDragState.End, finalPosition, _lastTextPosition!); + renderEditor.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition!); _startCaretRect = null; _lastTextPosition = null; _pointOffsetOrigin = null; _lastBoundedOffset = null; } else { final lerpValue = floatingCursorResetController.value; - final lerpX = - lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!; - final lerpY = - lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!; + final lerpX = lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!; + final lerpY = lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!; - renderEditor.setFloatingCursor(FloatingCursorDragState.Update, - Offset(lerpX, lerpY), _lastTextPosition!, + renderEditor.setFloatingCursor(FloatingCursorDragState.Update, Offset(lerpX, lerpY), _lastTextPosition!, resetLerpValue: lerpValue); } } @@ -382,8 +405,93 @@ mixin RawEditorStateTextInputClientMixin on EditorState final size = renderEditor.size; final transform = renderEditor.getTransformTo(null); _textInputConnection?.setEditableSizeAndTransform(size, transform); - SchedulerBinding.instance - .addPostFrameCallback((_) => _updateSizeAndTransform()); + SchedulerBinding.instance.addPostFrameCallback((_) => _updateSizeAndTransform()); + } + } +} + +extension on TextEditingDelta { + TextEditingDelta format() { + if (this is TextEditingDeltaInsertion) { + return (this as TextEditingDeltaInsertion).format(); + } else if (this is TextEditingDeltaDeletion) { + return (this as TextEditingDeltaDeletion).format(); + } else if (this is TextEditingDeltaReplacement) { + return (this as TextEditingDeltaReplacement).format(); + } else if (this is TextEditingDeltaNonTextUpdate) { + return (this as TextEditingDeltaNonTextUpdate).format(); + } + throw UnimplementedError(); + } +} + +const String _whitespace = ' '; +const int _len = _whitespace.length; + +extension on TextSelection { + TextSelection operator <<(int shiftAmount) => shift(-shiftAmount); + + TextSelection shift(int shiftAmount) => TextSelection( + baseOffset: max(0, baseOffset + shiftAmount), + extentOffset: max(0, extentOffset + shiftAmount), + ); +} + +extension on TextEditingDeltaInsertion { + TextEditingDeltaInsertion format() => TextEditingDeltaInsertion( + oldText: oldText << _len, + textInserted: textInserted, + insertionOffset: insertionOffset - _len, + selection: selection << _len, + composing: composing << _len, + ); +} + +extension on TextEditingDeltaDeletion { + TextEditingDeltaDeletion format() => TextEditingDeltaDeletion( + oldText: oldText << _len, + deletedRange: deletedRange << _len, + selection: selection << _len, + composing: composing << _len, + ); +} + +extension on TextEditingDeltaReplacement { + TextEditingDeltaReplacement format() => TextEditingDeltaReplacement( + oldText: oldText << _len, + replacementText: replacementText, + replacedRange: replacedRange << _len, + selection: selection << _len, + composing: composing << _len, + ); +} + +extension on TextEditingDeltaNonTextUpdate { + TextEditingDeltaNonTextUpdate format() => TextEditingDeltaNonTextUpdate( + oldText: oldText << _len, + selection: selection << _len, + composing: composing << _len, + ); +} + +extension on TextRange { + TextRange operator <<(int shiftAmount) => shift(-shiftAmount); + + TextRange shift(int shiftAmount) => !isValid + ? this + : TextRange( + start: max(0, start + shiftAmount), + end: max(0, end + shiftAmount), + ); +} + +extension on String { + String operator <<(int shiftAmount) => shift(shiftAmount); + + String shift(int shiftAmount) { + if (shiftAmount > length) { + return ''; } + return substring(shiftAmount); } } diff --git a/lib/src/editor/raw_editor/keyboard_shortcuts/editor_keyboard_shortcuts.dart b/lib/src/editor/raw_editor/keyboard_shortcuts/editor_keyboard_shortcuts.dart index a21b612c4..898c9be63 100644 --- a/lib/src/editor/raw_editor/keyboard_shortcuts/editor_keyboard_shortcuts.dart +++ b/lib/src/editor/raw_editor/keyboard_shortcuts/editor_keyboard_shortcuts.dart @@ -26,7 +26,6 @@ class EditorKeyboardShortcuts extends StatelessWidget { required this.controller, required this.readOnly, required this.enableAlwaysIndentOnTab, - required this.characterEvents, required this.spaceEvents, this.onKeyPressed, this.customShortcuts, @@ -39,7 +38,6 @@ class EditorKeyboardShortcuts extends StatelessWidget { final QuillController controller; @experimental final KeyEventResult? Function(KeyEvent event, Node? node)? onKeyPressed; - final List characterEvents; final List spaceEvents; final Map? customShortcuts; final Map>? customActions; @@ -97,17 +95,6 @@ class EditorKeyboardShortcuts extends StatelessWidget { final isSpace = event.logicalKey == LogicalKeyboardKey.space; final containsSelection = controller.selection.baseOffset != controller.selection.extentOffset; - if (!isTab && !isSpace && event.character != '\n' && !containsSelection) { - for (final charEvents in characterEvents) { - if (event.character != null && - event.character == charEvents.character) { - final executed = charEvents.execute(controller); - if (executed) { - return KeyEventResult.handled; - } - } - } - } if (event is! KeyDownEvent) { return KeyEventResult.ignored; diff --git a/lib/src/editor/raw_editor/raw_editor_state.dart b/lib/src/editor/raw_editor/raw_editor_state.dart index c6bcfa4a7..19aa26955 100644 --- a/lib/src/editor/raw_editor/raw_editor_state.dart +++ b/lib/src/editor/raw_editor/raw_editor_state.dart @@ -24,6 +24,7 @@ import '../../document/nodes/block.dart'; import '../../document/nodes/line.dart'; import '../../document/nodes/node.dart'; import '../editor.dart'; +import '../input/text_editor_input_client_mixin.dart'; import '../widgets/cursor.dart'; import '../widgets/default_styles.dart'; import '../widgets/link.dart'; @@ -36,7 +37,6 @@ import 'keyboard_shortcuts/editor_keyboard_shortcuts.dart'; import 'raw_editor.dart'; import 'raw_editor_render_object.dart'; import 'raw_editor_state_selection_delegate_mixin.dart'; -import 'raw_editor_state_text_input_client_mixin.dart'; import 'scribble_focusable.dart'; class QuillRawEditorState extends EditorState @@ -44,7 +44,7 @@ class QuillRawEditorState extends EditorState AutomaticKeepAliveClientMixin, WidgetsBindingObserver, TickerProviderStateMixin, - RawEditorStateTextInputClientMixin, + TextEditorInputClientMixin, RawEditorStateSelectionDelegateMixin { late final EditorKeyboardShortcutsActionsManager _shortcutActionsManager; @@ -486,7 +486,6 @@ class QuillRawEditorState extends EditorState child: EditorKeyboardShortcuts( actions: _shortcutActionsManager.actions, onKeyPressed: widget.config.onKeyPressed, - characterEvents: widget.config.characterShortcutEvents, spaceEvents: widget.config.spaceShortcutEvents, constraints: constraints, focusNode: widget.config.focusNode, From b81246068faab5a5b40bb1acde17af5cbd9ff28d Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Tue, 11 Mar 2025 19:54:13 -0400 Subject: [PATCH 02/40] Chore: minor changes --- lib/src/editor/input/diff_services.dart | 1 + lib/src/editor/input/ime/on_non_update_text.dart | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/editor/input/diff_services.dart b/lib/src/editor/input/diff_services.dart index 95e2c5555..26cbfbbc9 100644 --- a/lib/src/editor/input/diff_services.dart +++ b/lib/src/editor/input/diff_services.dart @@ -1,6 +1,7 @@ import 'package:flutter/services.dart'; import '../../delta/delta_diff.dart'; +/// Return a list of the change type that was do it to the content of the editor List getTextEditingDeltas( TextEditingValue? oldValue, TextEditingValue newValue, diff --git a/lib/src/editor/input/ime/on_non_update_text.dart b/lib/src/editor/input/ime/on_non_update_text.dart index 2d8b0455f..d816416ee 100644 --- a/lib/src/editor/input/ime/on_non_update_text.dart +++ b/lib/src/editor/input/ime/on_non_update_text.dart @@ -10,7 +10,6 @@ Future onNonTextUpdate( // // when typing characters with CJK IME on Windows, a non-text update is sent // with the selection range. - if (Platform.isWindows) { if (nonTextUpdate.composing == TextRange.empty && nonTextUpdate.selection.isCollapsed) { controller.updateSelection( From 7685b579c6606581b761fa37ec203bdb93a9b66d Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Tue, 11 Mar 2025 19:58:05 -0400 Subject: [PATCH 03/40] Chore: renamed input client to the old name of the text input clinet --- .../input/debounce/debounce.dart | 0 .../{ => raw_editor}/input/diff_services.dart | 2 +- .../{ => raw_editor}/input/ime/on_delete.dart | 0 .../{ => raw_editor}/input/ime/on_insert.dart | 10 +- .../input/ime/on_non_update_text.dart | 3 +- .../input/ime/on_replace_method.dart | 13 +- .../raw_editor_state_input_client_mixin.dart} | 115 +++++++++++------- .../editor/raw_editor/raw_editor_state.dart | 4 +- 8 files changed, 91 insertions(+), 56 deletions(-) rename lib/src/editor/{ => raw_editor}/input/debounce/debounce.dart (100%) rename lib/src/editor/{ => raw_editor}/input/diff_services.dart (97%) rename lib/src/editor/{ => raw_editor}/input/ime/on_delete.dart (100%) rename lib/src/editor/{ => raw_editor}/input/ime/on_insert.dart (64%) rename lib/src/editor/{ => raw_editor}/input/ime/on_non_update_text.dart (88%) rename lib/src/editor/{ => raw_editor}/input/ime/on_replace_method.dart (81%) rename lib/src/editor/{input/text_editor_input_client_mixin.dart => raw_editor/input/raw_editor_state_input_client_mixin.dart} (81%) diff --git a/lib/src/editor/input/debounce/debounce.dart b/lib/src/editor/raw_editor/input/debounce/debounce.dart similarity index 100% rename from lib/src/editor/input/debounce/debounce.dart rename to lib/src/editor/raw_editor/input/debounce/debounce.dart diff --git a/lib/src/editor/input/diff_services.dart b/lib/src/editor/raw_editor/input/diff_services.dart similarity index 97% rename from lib/src/editor/input/diff_services.dart rename to lib/src/editor/raw_editor/input/diff_services.dart index 26cbfbbc9..fe903e681 100644 --- a/lib/src/editor/input/diff_services.dart +++ b/lib/src/editor/raw_editor/input/diff_services.dart @@ -1,5 +1,5 @@ import 'package:flutter/services.dart'; -import '../../delta/delta_diff.dart'; +import '../../../delta/delta_diff.dart'; /// Return a list of the change type that was do it to the content of the editor List getTextEditingDeltas( diff --git a/lib/src/editor/input/ime/on_delete.dart b/lib/src/editor/raw_editor/input/ime/on_delete.dart similarity index 100% rename from lib/src/editor/input/ime/on_delete.dart rename to lib/src/editor/raw_editor/input/ime/on_delete.dart diff --git a/lib/src/editor/input/ime/on_insert.dart b/lib/src/editor/raw_editor/input/ime/on_insert.dart similarity index 64% rename from lib/src/editor/input/ime/on_insert.dart rename to lib/src/editor/raw_editor/input/ime/on_insert.dart index b5d0fe0f6..88f8d6745 100644 --- a/lib/src/editor/input/ime/on_insert.dart +++ b/lib/src/editor/raw_editor/input/ime/on_insert.dart @@ -1,7 +1,7 @@ import 'package:flutter/services.dart'; -import '../../../controller/quill_controller.dart'; -import '../../raw_editor/config/events/character_shortcuts_events.dart'; +import '../../../../controller/quill_controller.dart'; +import '../../../raw_editor/config/events/character_shortcuts_events.dart'; Future onInsert( TextEditingDeltaInsertion insertion, @@ -14,7 +14,8 @@ Future onInsert( if (insertionText.length == 1 && !insertionText.contains('\n')) { for (final shortcutEvent in characterShortcutEvents) { - if (shortcutEvent.character == insertionText && shortcutEvent.handler(controller)) { + if (shortcutEvent.character == insertionText && + shortcutEvent.handler(controller)) { return; } } @@ -24,6 +25,7 @@ Future onInsert( selection.baseOffset, selection.extentOffset - selection.baseOffset, insertionText, - TextSelection.collapsed(offset: selection.extentOffset + insertionText.length), + TextSelection.collapsed( + offset: selection.extentOffset + insertionText.length), ); } diff --git a/lib/src/editor/input/ime/on_non_update_text.dart b/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart similarity index 88% rename from lib/src/editor/input/ime/on_non_update_text.dart rename to lib/src/editor/raw_editor/input/ime/on_non_update_text.dart index d816416ee..16a0d04c5 100644 --- a/lib/src/editor/input/ime/on_non_update_text.dart +++ b/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart @@ -11,7 +11,8 @@ Future onNonTextUpdate( // when typing characters with CJK IME on Windows, a non-text update is sent // with the selection range. if (Platform.isWindows) { - if (nonTextUpdate.composing == TextRange.empty && nonTextUpdate.selection.isCollapsed) { + if (nonTextUpdate.composing == TextRange.empty && + nonTextUpdate.selection.isCollapsed) { controller.updateSelection( TextSelection.collapsed( offset: nonTextUpdate.selection.start, diff --git a/lib/src/editor/input/ime/on_replace_method.dart b/lib/src/editor/raw_editor/input/ime/on_replace_method.dart similarity index 81% rename from lib/src/editor/input/ime/on_replace_method.dart rename to lib/src/editor/raw_editor/input/ime/on_replace_method.dart index 184f12507..867b8cbff 100644 --- a/lib/src/editor/input/ime/on_replace_method.dart +++ b/lib/src/editor/raw_editor/input/ime/on_replace_method.dart @@ -2,8 +2,8 @@ import 'dart:io'; import 'package:flutter/services.dart'; -import '../../../controller/quill_controller.dart'; -import '../../raw_editor/config/events/character_shortcuts_events.dart'; +import '../../../../controller/quill_controller.dart'; +import '../../../raw_editor/config/events/character_shortcuts_events.dart'; import 'on_insert.dart'; Future onReplace( @@ -19,7 +19,8 @@ Future onReplace( if (selection.isCollapsed) { if (textReplacement.length == 1) { for (final shortcutEvent in characterShortcutEvents) { - if (shortcutEvent.character == textReplacement && shortcutEvent.handler(controller)) { + if (shortcutEvent.character == textReplacement && + shortcutEvent.handler(controller)) { return; } } @@ -30,7 +31,8 @@ Future onReplace( if (textReplacement.endsWith('\n')) { replacement = TextEditingDeltaReplacement( oldText: replacement.oldText, - replacementText: replacement.replacementText.substring(0, replacement.replacementText.length - 1), + replacementText: replacement.replacementText + .substring(0, replacement.replacementText.length - 1), replacedRange: replacement.replacedRange, selection: replacement.selection, composing: replacement.composing, @@ -45,8 +47,7 @@ Future onReplace( length, textReplacement, TextSelection.collapsed( - offset: replacement.selection.baseOffset + textReplacement.length - ), + offset: replacement.selection.baseOffset + textReplacement.length), ); } else { controller.replaceText( diff --git a/lib/src/editor/input/text_editor_input_client_mixin.dart b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart similarity index 81% rename from lib/src/editor/input/text_editor_input_client_mixin.dart rename to lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart index 0ead0d778..94cd8df09 100644 --- a/lib/src/editor/input/text_editor_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart @@ -8,8 +8,7 @@ import 'package:flutter/foundation.dart' show ValueNotifier, kIsWeb; import 'package:flutter/material.dart' show Theme; import 'package:flutter/scheduler.dart' show SchedulerBinding; import 'package:flutter/services.dart'; -import '../../document/document.dart'; -import '../raw_editor/raw_editor.dart'; +import '../raw_editor.dart'; import 'debounce/debounce.dart'; import 'diff_services.dart'; import 'ime/on_delete.dart'; @@ -17,7 +16,8 @@ import 'ime/on_insert.dart'; import 'ime/on_non_update_text.dart'; import 'ime/on_replace_method.dart'; -mixin TextEditorInputClientMixin on EditorState implements TextInputClient { +mixin RawEditorStateTextInputClientMixin on EditorState + implements TextInputClient { TextInputConnection? _textInputConnection; TextEditingValue? __lastKnownRemoteTextEditingValue; @@ -28,7 +28,8 @@ mixin TextEditorInputClientMixin on EditorState implements TextInputClient { } } - TextEditingValue? get _lastKnownRemoteTextEditingValue => __lastKnownRemoteTextEditingValue; + TextEditingValue? get _lastKnownRemoteTextEditingValue => + __lastKnownRemoteTextEditingValue; /// The range of text that is currently being composed. final ValueNotifier composingRange = ValueNotifier( @@ -51,12 +52,14 @@ mixin TextEditorInputClientMixin on EditorState implements TextInputClient { bool get shouldCreateInputConnection => kIsWeb || !widget.config.readOnly; /// Returns `true` if there is open input connection. - bool get hasConnection => _textInputConnection != null && _textInputConnection!.attached; + bool get hasConnection => + _textInputConnection != null && _textInputConnection!.attached; /// Opens or closes input connection based on the current state of /// [focusNode] and [value]. void openOrCloseConnection() { - if (widget.config.focusNode.hasFocus && widget.config.focusNode.consumeKeyboardToken()) { + if (widget.config.focusNode.hasFocus && + widget.config.focusNode.consumeKeyboardToken()) { openConnectionIfNeeded(); } else if (!widget.config.focusNode.hasFocus) { closeConnectionIfNeeded(); @@ -95,10 +98,14 @@ mixin TextEditorInputClientMixin on EditorState implements TextInputClient { /// Trap selection extends off end of document if (_lastKnownRemoteTextEditingValue != null) { - if (_lastKnownRemoteTextEditingValue!.selection.end > _lastKnownRemoteTextEditingValue!.text.length) { - _lastKnownRemoteTextEditingValue = _lastKnownRemoteTextEditingValue!.copyWith( - selection: _lastKnownRemoteTextEditingValue!.selection - .copyWith(extentOffset: _lastKnownRemoteTextEditingValue!.text.length)); + if (_lastKnownRemoteTextEditingValue!.selection.end > + _lastKnownRemoteTextEditingValue!.text.length) { + _lastKnownRemoteTextEditingValue = _lastKnownRemoteTextEditingValue! + .copyWith( + selection: _lastKnownRemoteTextEditingValue!.selection + .copyWith( + extentOffset: + _lastKnownRemoteTextEditingValue!.text.length)); } } _textInputConnection!.setEditingState(_lastKnownRemoteTextEditingValue!); @@ -108,12 +115,14 @@ mixin TextEditorInputClientMixin on EditorState implements TextInputClient { // windows void _updateComposingRectIfNeeded() { - final composingRange = _lastKnownRemoteTextEditingValue?.composing ?? textEditingValue.composing; + final composingRange = _lastKnownRemoteTextEditingValue?.composing ?? + textEditingValue.composing; if (hasConnection) { assert(mounted); if (composingRange.isValid) { final offset = composingRange.start; - final composingRect = renderEditor.getLocalRectForCaret(TextPosition(offset: offset)); + final composingRect = + renderEditor.getLocalRectForCaret(TextPosition(offset: offset)); _textInputConnection!.setComposingRect(composingRect); } //SchedulerBinding.instance.addPostFrameCallback((_) => _updateComposingRectIfNeeded()); @@ -123,9 +132,13 @@ mixin TextEditorInputClientMixin on EditorState implements TextInputClient { // macos void _updateCaretRectIfNeeded() { if (hasConnection) { - if (!dirty && renderEditor.selection.isValid && renderEditor.selection.isCollapsed) { - final currentTextPosition = TextPosition(offset: renderEditor.selection.baseOffset); - final caretRect = renderEditor.getLocalRectForCaret(currentTextPosition); + if (!dirty && + renderEditor.selection.isValid && + renderEditor.selection.isCollapsed) { + final currentTextPosition = + TextPosition(offset: renderEditor.selection.baseOffset); + final caretRect = + renderEditor.getLocalRectForCaret(currentTextPosition); _textInputConnection!.setCaretRect(caretRect); } //SchedulerBinding.instance.addPostFrameCallback((_) => _updateCaretRectIfNeeded()); @@ -179,7 +192,8 @@ mixin TextEditorInputClientMixin on EditorState implements TextInputClient { // Start TextInputClient implementation @override - TextEditingValue? get currentTextEditingValue => _lastKnownRemoteTextEditingValue; + TextEditingValue? get currentTextEditingValue => + _lastKnownRemoteTextEditingValue; // autofill is not needed @override @@ -260,12 +274,13 @@ mixin TextEditorInputClientMixin on EditorState implements TextInputClient { if (delta is TextEditingDeltaNonTextUpdate) { composingRange.value = delta.composing; } else { - composingRange.value = composingRange.value.start != -1 && delta.composing.end != -1 - ? TextRange( - start: composingRange.value.start, - end: delta.composing.end, - ) - : delta.composing; + composingRange.value = + composingRange.value.start != -1 && delta.composing.end != -1 + ? TextRange( + start: composingRange.value.start, + end: delta.composing.end, + ) + : delta.composing; } // solve the issue where the Chinese IME doesn't continue deleting after the input content has been deleted. @@ -319,39 +334,49 @@ mixin TextEditorInputClientMixin on EditorState implements TextInputClient { // we cache the position. _pointOffsetOrigin = point.offset; - final currentTextPosition = TextPosition(offset: renderEditor.selection.baseOffset); - _startCaretRect = renderEditor.getLocalRectForCaret(currentTextPosition); + final currentTextPosition = + TextPosition(offset: renderEditor.selection.baseOffset); + _startCaretRect = + renderEditor.getLocalRectForCaret(currentTextPosition); - _lastBoundedOffset = _startCaretRect!.center - _floatingCursorOffset(currentTextPosition); + _lastBoundedOffset = _startCaretRect!.center - + _floatingCursorOffset(currentTextPosition); _lastTextPosition = currentTextPosition; - renderEditor.setFloatingCursor(point.state, _lastBoundedOffset!, _lastTextPosition!); + renderEditor.setFloatingCursor( + point.state, _lastBoundedOffset!, _lastTextPosition!); break; case FloatingCursorDragState.Update: assert(_lastTextPosition != null, 'Last text position was not set'); final floatingCursorOffset = _floatingCursorOffset(_lastTextPosition!); final centeredPoint = point.offset! - _pointOffsetOrigin!; - final rawCursorOffset = _startCaretRect!.center + centeredPoint - floatingCursorOffset; + final rawCursorOffset = + _startCaretRect!.center + centeredPoint - floatingCursorOffset; - final preferredLineHeight = renderEditor.preferredLineHeight(_lastTextPosition!); + final preferredLineHeight = + renderEditor.preferredLineHeight(_lastTextPosition!); _lastBoundedOffset = renderEditor.calculateBoundedFloatingCursorOffset( rawCursorOffset, preferredLineHeight, ); - _lastTextPosition = renderEditor - .getPositionForOffset(renderEditor.localToGlobal(_lastBoundedOffset! + floatingCursorOffset)); - renderEditor.setFloatingCursor(point.state, _lastBoundedOffset!, _lastTextPosition!); - final newSelection = - TextSelection.collapsed(offset: _lastTextPosition!.offset, affinity: _lastTextPosition!.affinity); + _lastTextPosition = renderEditor.getPositionForOffset(renderEditor + .localToGlobal(_lastBoundedOffset! + floatingCursorOffset)); + renderEditor.setFloatingCursor( + point.state, _lastBoundedOffset!, _lastTextPosition!); + final newSelection = TextSelection.collapsed( + offset: _lastTextPosition!.offset, + affinity: _lastTextPosition!.affinity); // Setting selection as floating cursor moves will have scroll view // bring background cursor into view - renderEditor.onSelectionChanged(newSelection, SelectionChangedCause.forcePress); + renderEditor.onSelectionChanged( + newSelection, SelectionChangedCause.forcePress); break; case FloatingCursorDragState.End: // We skip animation if no update has happened. if (_lastTextPosition != null && _lastBoundedOffset != null) { floatingCursorResetController ..value = 0.0 - ..animateTo(1, duration: _floatingCursorResetTime, curve: Curves.decelerate); + ..animateTo(1, + duration: _floatingCursorResetTime, curve: Curves.decelerate); } break; } @@ -364,20 +389,25 @@ mixin TextEditorInputClientMixin on EditorState implements TextInputClient { /// and repositioned (linear interpolation between position of floating cursor /// and current position of background cursor) void onFloatingCursorResetTick() { - final finalPosition = renderEditor.getLocalRectForCaret(_lastTextPosition!).centerLeft - - _floatingCursorOffset(_lastTextPosition!); + final finalPosition = + renderEditor.getLocalRectForCaret(_lastTextPosition!).centerLeft - + _floatingCursorOffset(_lastTextPosition!); if (floatingCursorResetController.isCompleted) { - renderEditor.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition!); + renderEditor.setFloatingCursor( + FloatingCursorDragState.End, finalPosition, _lastTextPosition!); _startCaretRect = null; _lastTextPosition = null; _pointOffsetOrigin = null; _lastBoundedOffset = null; } else { final lerpValue = floatingCursorResetController.value; - final lerpX = lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!; - final lerpY = lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!; + final lerpX = + lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!; + final lerpY = + lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!; - renderEditor.setFloatingCursor(FloatingCursorDragState.Update, Offset(lerpX, lerpY), _lastTextPosition!, + renderEditor.setFloatingCursor(FloatingCursorDragState.Update, + Offset(lerpX, lerpY), _lastTextPosition!, resetLerpValue: lerpValue); } } @@ -405,7 +435,8 @@ mixin TextEditorInputClientMixin on EditorState implements TextInputClient { final size = renderEditor.size; final transform = renderEditor.getTransformTo(null); _textInputConnection?.setEditableSizeAndTransform(size, transform); - SchedulerBinding.instance.addPostFrameCallback((_) => _updateSizeAndTransform()); + SchedulerBinding.instance + .addPostFrameCallback((_) => _updateSizeAndTransform()); } } } diff --git a/lib/src/editor/raw_editor/raw_editor_state.dart b/lib/src/editor/raw_editor/raw_editor_state.dart index 19aa26955..3a104ab20 100644 --- a/lib/src/editor/raw_editor/raw_editor_state.dart +++ b/lib/src/editor/raw_editor/raw_editor_state.dart @@ -24,7 +24,6 @@ import '../../document/nodes/block.dart'; import '../../document/nodes/line.dart'; import '../../document/nodes/node.dart'; import '../editor.dart'; -import '../input/text_editor_input_client_mixin.dart'; import '../widgets/cursor.dart'; import '../widgets/default_styles.dart'; import '../widgets/link.dart'; @@ -32,6 +31,7 @@ import '../widgets/proxy.dart'; import '../widgets/text/text_block.dart'; import '../widgets/text/text_line.dart'; import '../widgets/text/text_selection.dart'; +import 'input/raw_editor_state_input_client_mixin.dart'; import 'keyboard_shortcuts/editor_keyboard_shortcut_actions_manager.dart'; import 'keyboard_shortcuts/editor_keyboard_shortcuts.dart'; import 'raw_editor.dart'; @@ -44,7 +44,7 @@ class QuillRawEditorState extends EditorState AutomaticKeepAliveClientMixin, WidgetsBindingObserver, TickerProviderStateMixin, - TextEditorInputClientMixin, + RawEditorStateTextInputClientMixin, RawEditorStateSelectionDelegateMixin { late final EditorKeyboardShortcutsActionsManager _shortcutActionsManager; From 5a3dab39a723270aa519a92029326ab1d8facb73 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Tue, 11 Mar 2025 20:13:43 -0400 Subject: [PATCH 04/40] Chore: removed unnecessary print --- lib/src/controller/quill_controller.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/src/controller/quill_controller.dart b/lib/src/controller/quill_controller.dart index 8e9b991bf..0d2d291a4 100644 --- a/lib/src/controller/quill_controller.dart +++ b/lib/src/controller/quill_controller.dart @@ -278,8 +278,6 @@ class QuillController extends ChangeNotifier { return; } - print('Remove params: len: $len, index: $index'); - Delta? delta; Style? style; if (len > 0 || data is! String || data.isNotEmpty) { From e039446b2380ab441d7b2457dc84895f4e00ab65 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Tue, 11 Mar 2025 20:22:10 -0400 Subject: [PATCH 05/40] Chore: moved internal editing extensions and change List deltas to a single delta --- .../raw_editor/input/diff_services.dart | 80 +++++------ .../text_editing_delta_formatters.dart | 130 ++++++++++++++++++ .../raw_editor_state_input_client_mixin.dart | 102 ++------------ 3 files changed, 176 insertions(+), 136 deletions(-) create mode 100644 lib/src/editor/raw_editor/input/formatters/text_editing_delta_formatters.dart diff --git a/lib/src/editor/raw_editor/input/diff_services.dart b/lib/src/editor/raw_editor/input/diff_services.dart index fe903e681..f2d474088 100644 --- a/lib/src/editor/raw_editor/input/diff_services.dart +++ b/lib/src/editor/raw_editor/input/diff_services.dart @@ -2,18 +2,16 @@ import 'package:flutter/services.dart'; import '../../../delta/delta_diff.dart'; /// Return a list of the change type that was do it to the content of the editor -List getTextEditingDeltas( +TextEditingDelta getTextEditingDelta( TextEditingValue? oldValue, TextEditingValue newValue, ) { if (oldValue == null || oldValue.text == newValue.text) { - return [ - TextEditingDeltaNonTextUpdate( - oldText: newValue.text, - selection: newValue.selection, - composing: newValue.composing, - ), - ]; + return TextEditingDeltaNonTextUpdate( + oldText: newValue.text, + selection: newValue.selection, + composing: newValue.composing, + ); } final currentText = oldValue.text; final diff = getDiff( @@ -22,48 +20,40 @@ List getTextEditingDeltas( newValue.selection.extentOffset, ); if (diff.inserted.isNotEmpty && diff.deleted.isEmpty) { - return [ - TextEditingDeltaInsertion( - oldText: currentText, - textInserted: diff.inserted, - insertionOffset: diff.start, - selection: newValue.selection, - composing: newValue.composing, - ), - ]; + return TextEditingDeltaInsertion( + oldText: currentText, + textInserted: diff.inserted, + insertionOffset: diff.start, + selection: newValue.selection, + composing: newValue.composing, + ); } else if (diff.inserted.isEmpty && diff.deleted.isNotEmpty) { - return [ - TextEditingDeltaDeletion( - oldText: currentText, - selection: newValue.selection, - composing: newValue.composing, - deletedRange: TextRange( - start: diff.start, - end: diff.start + diff.deleted.length, - ), + return TextEditingDeltaDeletion( + oldText: currentText, + selection: newValue.selection, + composing: newValue.composing, + deletedRange: TextRange( + start: diff.start, + end: diff.start + diff.deleted.length, ), - ]; + ); } else if (diff.inserted.isNotEmpty && diff.deleted.isNotEmpty) { - return [ - TextEditingDeltaReplacement( - oldText: currentText, - selection: newValue.selection, - composing: newValue.composing, - replacementText: diff.inserted, - replacedRange: TextRange( - start: diff.start, - end: diff.start + diff.deleted.length, - ), + return TextEditingDeltaReplacement( + oldText: currentText, + selection: newValue.selection, + composing: newValue.composing, + replacementText: diff.inserted, + replacedRange: TextRange( + start: diff.start, + end: diff.start + diff.deleted.length, ), - ]; + ); } else if (diff.inserted.isEmpty && diff.deleted.isEmpty) { - return [ - TextEditingDeltaNonTextUpdate( - oldText: newValue.text, - selection: newValue.selection, - composing: newValue.composing, - ), - ]; + return TextEditingDeltaNonTextUpdate( + oldText: newValue.text, + selection: newValue.selection, + composing: newValue.composing, + ); } throw UnsupportedError('Unknown diff: $diff'); } diff --git a/lib/src/editor/raw_editor/input/formatters/text_editing_delta_formatters.dart b/lib/src/editor/raw_editor/input/formatters/text_editing_delta_formatters.dart new file mode 100644 index 000000000..f3147f5e4 --- /dev/null +++ b/lib/src/editor/raw_editor/input/formatters/text_editing_delta_formatters.dart @@ -0,0 +1,130 @@ +import 'dart:math'; +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; + +const String _whitespace = ' '; +const int _whitespaceLen = _whitespace.length; + +// Extension on TextEditingDelta to provide a generic formatting method. +// This method checks the type of the TextEditingDelta and calls the appropriate +// formatting method for the specific delta type (insertion, deletion, replacement, or non-text update). +// If the delta type is not recognized, it throws an UnimplementedError. +@internal +@experimental +extension GeneralTextEditingFormatter on TextEditingDelta { + TextEditingDelta format() { + if (this is TextEditingDeltaInsertion) { + return (this as TextEditingDeltaInsertion).format(); + } else if (this is TextEditingDeltaDeletion) { + return (this as TextEditingDeltaDeletion).format(); + } else if (this is TextEditingDeltaReplacement) { + return (this as TextEditingDeltaReplacement).format(); + } else if (this is TextEditingDeltaNonTextUpdate) { + return (this as TextEditingDeltaNonTextUpdate).format(); + } + throw UnimplementedError(); + } +} + +// Extension on TextEditingDeltaInsertion to format insertion deltas. +// Adjusts the oldText, insertionOffset, selection, and composing properties +// by shifting them based on a predefined whitespace length. +@internal +@experimental +extension TextInsertionFormatter on TextEditingDeltaInsertion { + TextEditingDeltaInsertion format() => TextEditingDeltaInsertion( + oldText: oldText << _whitespaceLen, + textInserted: textInserted, + insertionOffset: insertionOffset - _whitespaceLen, + selection: selection << _whitespaceLen, + composing: composing << _whitespaceLen, + ); +} + +// Extension on TextEditingDeltaDeletion to format deletion deltas. +// Adjusts the oldText, deletedRange, selection, and composing properties +// by shifting them based on a predefined whitespace length. +@internal +@experimental +extension TextDeletionFormatter on TextEditingDeltaDeletion { + TextEditingDeltaDeletion format() => TextEditingDeltaDeletion( + oldText: oldText << _whitespaceLen, + deletedRange: deletedRange << _whitespaceLen, + selection: selection << _whitespaceLen, + composing: composing << _whitespaceLen, + ); +} + +// Extension on TextEditingDeltaReplacement to format replacement deltas. +// Adjusts the oldText, replacedRange, selection, and composing properties +// by shifting them based on a predefined whitespace length. +@internal +@experimental +extension TextReplacementFormatter on TextEditingDeltaReplacement { + TextEditingDeltaReplacement format() => TextEditingDeltaReplacement( + oldText: oldText << _whitespaceLen, + replacementText: replacementText, + replacedRange: replacedRange << _whitespaceLen, + selection: selection << _whitespaceLen, + composing: composing << _whitespaceLen, + ); +} + +// Extension on TextEditingDeltaNonTextUpdate to format non-text update deltas. +// Adjusts the oldText, selection, and composing properties +// by shifting them based on a predefined whitespace length. +@internal +@experimental +extension NonTextUpdateFormatter on TextEditingDeltaNonTextUpdate { + TextEditingDeltaNonTextUpdate format() => TextEditingDeltaNonTextUpdate( + oldText: oldText << _whitespaceLen, + selection: selection << _whitespaceLen, + composing: composing << _whitespaceLen, + ); +} + +// Extension on TextRange to provide shifting functionality. +// Allows shifting the start and end positions of a TextRange by a specified amount. +// If the range is invalid, it returns the original range. +@internal +@experimental +extension ShiftTextRange on TextRange { + TextRange operator <<(int shiftAmount) => shift(-shiftAmount); + + TextRange shift(int shiftAmount) => !isValid + ? this + : TextRange( + start: max(0, start + shiftAmount), + end: max(0, end + shiftAmount), + ); +} + +// Extension on String to provide shifting functionality. +// Allows shifting the string by removing a specified number of characters from the beginning. +// If the shift amount is greater than the string length, it returns an empty string. +@internal +@experimental +extension ShiftString on String { + String operator <<(int shiftAmount) => shift(shiftAmount); + + String shift(int shiftAmount) { + if (shiftAmount > length) { + return ''; + } + return substring(shiftAmount); + } +} + +// Extension on TextSelection to provide shifting functionality. +// Allows shifting the baseOffset and extentOffset of a TextSelection by a specified amount. +// Ensures the offsets do not go below zero. +@internal +@experimental +extension ShiftTextSelection on TextSelection { + TextSelection operator <<(int shiftAmount) => shift(-shiftAmount); + + TextSelection shift(int shiftAmount) => TextSelection( + baseOffset: max(0, baseOffset + shiftAmount), + extentOffset: max(0, extentOffset + shiftAmount), + ); +} diff --git a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart index 94cd8df09..008dc7a22 100644 --- a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:math'; import 'dart:ui' show lerpDouble; import 'package:flutter/animation.dart' show Curves; @@ -11,6 +10,7 @@ import 'package:flutter/services.dart'; import '../raw_editor.dart'; import 'debounce/debounce.dart'; import 'diff_services.dart'; +import 'formatters/text_editing_delta_formatters.dart'; import 'ime/on_delete.dart'; import 'ime/on_insert.dart'; import 'ime/on_non_update_text.dart'; @@ -222,7 +222,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState return; } - final deltas = getTextEditingDeltas(currentTextEditingValue, value); + final textEditingDlta = getTextEditingDelta(currentTextEditingValue, value); _lastKnownRemoteTextEditingValue = value; // On mobile, the IME will send a lot of updateEditingValue events, so we // need to debounce it to combine them together. @@ -234,13 +234,17 @@ mixin RawEditorStateTextInputClientMixin on EditorState ) : Duration.zero, () { - _apply(deltas); + _apply([textEditingDlta]); }, ); } Future _apply(List deltas) async { - final formattedDeltas = deltas.map((e) => e.format()).toList(); + final formattedDeltas = deltas + .map( + (e) => e.format(), + ) + .toList(); for (final delta in formattedDeltas) { _updateComposing(delta); @@ -283,7 +287,9 @@ mixin RawEditorStateTextInputClientMixin on EditorState : delta.composing; } - // solve the issue where the Chinese IME doesn't continue deleting after the input content has been deleted. + // solve an issue where the Chinese + // IME doesn't continue deleting after + // the input content has been deleted. if (Platform.isMacOS && (composingRange.value.isCollapsed)) { composingRange.value = TextRange.empty; } @@ -440,89 +446,3 @@ mixin RawEditorStateTextInputClientMixin on EditorState } } } - -extension on TextEditingDelta { - TextEditingDelta format() { - if (this is TextEditingDeltaInsertion) { - return (this as TextEditingDeltaInsertion).format(); - } else if (this is TextEditingDeltaDeletion) { - return (this as TextEditingDeltaDeletion).format(); - } else if (this is TextEditingDeltaReplacement) { - return (this as TextEditingDeltaReplacement).format(); - } else if (this is TextEditingDeltaNonTextUpdate) { - return (this as TextEditingDeltaNonTextUpdate).format(); - } - throw UnimplementedError(); - } -} - -const String _whitespace = ' '; -const int _len = _whitespace.length; - -extension on TextSelection { - TextSelection operator <<(int shiftAmount) => shift(-shiftAmount); - - TextSelection shift(int shiftAmount) => TextSelection( - baseOffset: max(0, baseOffset + shiftAmount), - extentOffset: max(0, extentOffset + shiftAmount), - ); -} - -extension on TextEditingDeltaInsertion { - TextEditingDeltaInsertion format() => TextEditingDeltaInsertion( - oldText: oldText << _len, - textInserted: textInserted, - insertionOffset: insertionOffset - _len, - selection: selection << _len, - composing: composing << _len, - ); -} - -extension on TextEditingDeltaDeletion { - TextEditingDeltaDeletion format() => TextEditingDeltaDeletion( - oldText: oldText << _len, - deletedRange: deletedRange << _len, - selection: selection << _len, - composing: composing << _len, - ); -} - -extension on TextEditingDeltaReplacement { - TextEditingDeltaReplacement format() => TextEditingDeltaReplacement( - oldText: oldText << _len, - replacementText: replacementText, - replacedRange: replacedRange << _len, - selection: selection << _len, - composing: composing << _len, - ); -} - -extension on TextEditingDeltaNonTextUpdate { - TextEditingDeltaNonTextUpdate format() => TextEditingDeltaNonTextUpdate( - oldText: oldText << _len, - selection: selection << _len, - composing: composing << _len, - ); -} - -extension on TextRange { - TextRange operator <<(int shiftAmount) => shift(-shiftAmount); - - TextRange shift(int shiftAmount) => !isValid - ? this - : TextRange( - start: max(0, start + shiftAmount), - end: max(0, end + shiftAmount), - ); -} - -extension on String { - String operator <<(int shiftAmount) => shift(shiftAmount); - - String shift(int shiftAmount) { - if (shiftAmount > length) { - return ''; - } - return substring(shiftAmount); - } -} From bcacc274e756c9d2418c9d09b4226a8f01c4987f Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Tue, 11 Mar 2025 20:24:23 -0400 Subject: [PATCH 06/40] Chore: removed unused import --- .../raw_editor/keyboard_shortcuts/editor_keyboard_shortcuts.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/editor/raw_editor/keyboard_shortcuts/editor_keyboard_shortcuts.dart b/lib/src/editor/raw_editor/keyboard_shortcuts/editor_keyboard_shortcuts.dart index 898c9be63..f1a128869 100644 --- a/lib/src/editor/raw_editor/keyboard_shortcuts/editor_keyboard_shortcuts.dart +++ b/lib/src/editor/raw_editor/keyboard_shortcuts/editor_keyboard_shortcuts.dart @@ -12,7 +12,6 @@ import '../../../document/nodes/leaf.dart' as leaf; import '../../../document/nodes/line.dart'; import '../../../document/nodes/node.dart'; import '../../widgets/keyboard_listener.dart'; -import '../config/events/character_shortcuts_events.dart'; import '../config/events/space_shortcut_events.dart'; import 'default_single_activator_intents.dart'; From a4d567a096444b114f7e2add4aca34139e318629 Mon Sep 17 00:00:00 2001 From: realth000 Date: Thu, 20 Feb 2025 17:40:14 +0800 Subject: [PATCH 07/40] doc: update controller length extension method deprecation message (#2483) * docs: update controller length extension method deprecation message * docs: update changelog --- CHANGELOG.md | 1 + .../lib/src/common/extensions/controller_ext.dart | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccebbc559..072da66b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed * Removed unicode from `QuillText` element that causes weird caret behavior on empty lines [#2453](https://github.com/singerdmx/flutter-quill/pull/2453). +* Update QuillController `length` extension method deprecation message [#2483](https://github.com/singerdmx/flutter-quill/pull/2483). ## [11.0.0] - 2025-02-16 diff --git a/flutter_quill_extensions/lib/src/common/extensions/controller_ext.dart b/flutter_quill_extensions/lib/src/common/extensions/controller_ext.dart index eb67e61c4..45443c4da 100644 --- a/flutter_quill_extensions/lib/src/common/extensions/controller_ext.dart +++ b/flutter_quill_extensions/lib/src/common/extensions/controller_ext.dart @@ -6,7 +6,7 @@ extension QuillControllerExt on QuillController { 'Invalid extension property and will be removed, use selection.baseOffset instead') int get index => selection.baseOffset; @Deprecated( - 'Invalid extension property and will be removed, use selection.baseOffset instead') + 'Invalid extension property and will be removed, use selection.extentOffset - selection.baseOffset instead') int get length => selection.extentOffset - index; @Deprecated('Invalid extension method and will be removed.') From 1a287b774e3345c652a3f3d10b07209947cb9b13 Mon Sep 17 00:00:00 2001 From: Ellet Date: Thu, 20 Feb 2025 13:07:18 +0300 Subject: [PATCH 08/40] chore: update GitHub bug template to require the package version input --- .github/ISSUE_TEMPLATE/1_bug.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/1_bug.yml b/.github/ISSUE_TEMPLATE/1_bug.yml index 8da2bfd73..0087a8bc0 100644 --- a/.github/ISSUE_TEMPLATE/1_bug.yml +++ b/.github/ISSUE_TEMPLATE/1_bug.yml @@ -17,10 +17,10 @@ body: - type: input attributes: label: Flutter Quill Version - description: The relevant package versions - placeholder: e.g., 10.0.0 + description: The package version that's used in pubspec.lock + placeholder: e.g., 11.0.0 validations: - required: false + required: true - type: textarea attributes: label: Steps to Reproduce @@ -70,4 +70,4 @@ body: validations: - required: false \ No newline at end of file + required: false From 54ccff3f35dfba3ea3785fb63274f321f4881ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20J=C3=A4rvstrand?= Date: Fri, 21 Feb 2025 03:04:10 +0100 Subject: [PATCH 09/40] Focus and open context menu on right click if unfocused (#2477) Co-authored-by: CatHood0 --- CHANGELOG.md | 3 ++- lib/src/editor/editor.dart | 23 +++++++++++++++++++++++ lib/src/editor/widgets/delegate.dart | 10 ++++++---- test/bug_fix_test.dart | 3 +++ 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 072da66b5..578eb1ec2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -* Removed unicode from `QuillText` element that causes weird caret behavior on empty lines [#2453](https://github.com/singerdmx/flutter-quill/pull/2453). +* Remove unicode from `QuillText` element that causes weird caret behavior on empty lines [#2453](https://github.com/singerdmx/flutter-quill/pull/2453). +* Focus and open context menu on right click if unfocused [#2477](https://github.com/singerdmx/flutter-quill/pull/2477). * Update QuillController `length` extension method deprecation message [#2483](https://github.com/singerdmx/flutter-quill/pull/2483). ## [11.0.0] - 2025-02-16 diff --git a/lib/src/editor/editor.dart b/lib/src/editor/editor.dart index 1641f1d6c..e1deaaf9e 100644 --- a/lib/src/editor/editor.dart +++ b/lib/src/editor/editor.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import '../common/utils/platform.dart'; @@ -557,6 +558,28 @@ class _QuillEditorSelectionGestureDetectorBuilder } } + /// onSingleTapUp for mouse right click + @override + void onSecondarySingleTapUp(TapUpDetails details) { + if (delegate.selectionEnabled && + renderEditor != null && + renderEditor!.selection.isCollapsed) { + renderEditor!.selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.longPress, + ); + } + + if (renderEditor?._hasFocus == false) { + _state._requestKeyboard(); + SchedulerBinding.instance.addPostFrameCallback((_) { + super.onSecondarySingleTapUp(details); + }); + } else { + super.onSecondarySingleTapUp(details); + } + } + @override void onSingleLongTapStart(LongPressStartDetails details) { if (_state.configurations.onSingleLongTapStart != null) { diff --git a/lib/src/editor/widgets/delegate.dart b/lib/src/editor/widgets/delegate.dart index c47a2d88e..e884a0d1d 100644 --- a/lib/src/editor/widgets/delegate.dart +++ b/lib/src/editor/widgets/delegate.dart @@ -199,10 +199,12 @@ class EditorTextSelectionGestureDetectorBuilder { /// onSingleTapUp for mouse right click @protected void onSecondarySingleTapUp(TapUpDetails details) { - // added to show toolbar by right click - if (checkSelectionToolbarShouldShow(isAdditionalAction: true)) { - editor!.showToolbar(); - } + SchedulerBinding.instance.addPostFrameCallback((_) { + // added to show toolbar by right click + if (checkSelectionToolbarShouldShow(isAdditionalAction: true)) { + editor!.showToolbar(); + } + }); } /// Handler for [EditorTextSelectionGestureDetector.onSingleTapCancel]. diff --git a/test/bug_fix_test.dart b/test/bug_fix_test.dart index 29d2c378d..95ffcb7ae 100644 --- a/test/bug_fix_test.dart +++ b/test/bug_fix_test.dart @@ -162,6 +162,9 @@ void main() { await tester.tap(find.byType(QuillEditor), buttons: kSecondaryButton, kind: device); await tester.pumpAndSettle(); + while (find.byType(AdaptiveTextSelectionToolbar).evaluate().isEmpty) { + await tester.pumpAndSettle(); + } // Verify custom widget shows expect(find.byType(AdaptiveTextSelectionToolbar), findsAny); From dfb7bbcae068587f0bfd513665af7ea8181f7b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20J=C3=A4rvstrand?= Date: Sat, 22 Feb 2025 03:27:45 +0100 Subject: [PATCH 10/40] Expose Rule type so that Document.setCustomRules can be used (#2484) --- CHANGELOG.md | 4 ++++ lib/flutter_quill.dart | 1 + 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 578eb1ec2..4159aa37c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Focus and open context menu on right click if unfocused [#2477](https://github.com/singerdmx/flutter-quill/pull/2477). * Update QuillController `length` extension method deprecation message [#2483](https://github.com/singerdmx/flutter-quill/pull/2483). +### Added + +* `Rule` is now part of the public API, so that `Document.setCustomRules` can be used. + ## [11.0.0] - 2025-02-16 > [!IMPORTANT] diff --git a/lib/flutter_quill.dart b/lib/flutter_quill.dart index 9f8dd2e5e..c11b0eeea 100644 --- a/lib/flutter_quill.dart +++ b/lib/flutter_quill.dart @@ -33,6 +33,7 @@ export 'src/editor_toolbar_controller_shared/copy_cut_service/copy_cut_service_p export 'src/editor_toolbar_controller_shared/copy_cut_service/default_copy_cut_service.dart'; export 'src/editor_toolbar_controller_shared/quill_config.dart'; export 'src/l10n/generated/quill_localizations.dart'; +export 'src/rules/rule.dart' show Rule; export 'src/toolbar/embed/embed_button_builder.dart'; export 'src/toolbar/simple_toolbar.dart'; export 'src/toolbar/structs/link_dialog_action.dart'; From eb91d240e37246b50d2a6a9fcd69d8fd9a70454e Mon Sep 17 00:00:00 2001 From: satotoshitaka11 <73925427+satotoshitaka11@users.noreply.github.com> Date: Tue, 25 Feb 2025 01:48:59 +0900 Subject: [PATCH 11/40] feat: Enable BoxDecoration for DefaultTextBlockStyle of header Attribute (#2438) * feat: Enable BoxDecoration for DefaultTextBlockStyle of header Attribute --------- Co-authored-by: Ellet --- CHANGELOG.md | 9 +++--- .../editor/raw_editor/raw_editor_state.dart | 32 +++++++++++++++++-- lib/src/editor/widgets/text/text_block.dart | 2 +- lib/src/editor/widgets/text/text_line.dart | 25 +++++++++++++-- 4 files changed, 58 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4159aa37c..4d4c095e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,13 +12,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -* Remove unicode from `QuillText` element that causes weird caret behavior on empty lines [#2453](https://github.com/singerdmx/flutter-quill/pull/2453). -* Focus and open context menu on right click if unfocused [#2477](https://github.com/singerdmx/flutter-quill/pull/2477). -* Update QuillController `length` extension method deprecation message [#2483](https://github.com/singerdmx/flutter-quill/pull/2483). +- Remove unicode from `QuillText` element that causes weird caret behavior on empty lines [#2453](https://github.com/singerdmx/flutter-quill/pull/2453). +- Focus and open context menu on right click if unfocused [#2477](https://github.com/singerdmx/flutter-quill/pull/2477). +- Update QuillController `length` extension method deprecation message [#2483](https://github.com/singerdmx/flutter-quill/pull/2483). ### Added -* `Rule` is now part of the public API, so that `Document.setCustomRules` can be used. +- `Rule` is now part of the public API, so that `Document.setCustomRules` can be used. +- `decoration` property in `DefaultTextBlockStyle` for the `header` attribute to customize headers with borders, background colors, and other styles using `BoxDecoration` [#2429](https://github.com/singerdmx/flutter-quill/pull/2429). ## [11.0.0] - 2025-02-16 diff --git a/lib/src/editor/raw_editor/raw_editor_state.dart b/lib/src/editor/raw_editor/raw_editor_state.dart index 3a104ab20..e9d660505 100644 --- a/lib/src/editor/raw_editor/raw_editor_state.dart +++ b/lib/src/editor/raw_editor/raw_editor_state.dart @@ -585,7 +585,8 @@ class QuillRawEditorState extends EditorState prevNodeOl = attrs[Attribute.list.key] == Attribute.ol; final nodeTextDirection = getDirectionOfNode(node, _textDirection); if (node is Line) { - final editableTextLine = _getEditableTextLineFromNode(node, context); + final editableTextLine = + _getEditableTextLineFromNode(node, context, attrs); result.add(Directionality( textDirection: nodeTextDirection, child: editableTextLine)); } else if (node is Block) { @@ -638,7 +639,7 @@ class QuillRawEditorState extends EditorState } EditableTextLine _getEditableTextLineFromNode( - Line node, BuildContext context) { + Line node, BuildContext context, Map> attrs) { final textLine = TextLine( line: node, textDirection: _textDirection, @@ -667,7 +668,8 @@ class QuillRawEditorState extends EditorState _hasFocus, MediaQuery.devicePixelRatioOf(context), _cursorCont, - _styles!.inlineCode!); + _styles!.inlineCode!, + _getDecoration(node, _styles, attrs)); return editableTextLine; } @@ -771,6 +773,30 @@ class QuillRawEditorState extends EditorState return VerticalSpacing.zero; } + BoxDecoration? _getDecoration(Node node, DefaultStyles? defaultStyles, + Map> attrs) { + if (attrs.containsKey(Attribute.header.key)) { + final level = attrs[Attribute.header.key]!.value; + switch (level) { + case 1: + return defaultStyles!.h1!.decoration; + case 2: + return defaultStyles!.h2!.decoration; + case 3: + return defaultStyles!.h3!.decoration; + case 4: + return defaultStyles!.h4!.decoration; + case 5: + return defaultStyles!.h5!.decoration; + case 6: + return defaultStyles!.h6!.decoration; + default: + throw ArgumentError('Invalid level $level'); + } + } + return null; + } + void _didChangeTextEditingValueListener() { _didChangeTextEditingValue(controller.ignoreFocusOnTextChange); } diff --git a/lib/src/editor/widgets/text/text_block.dart b/lib/src/editor/widgets/text/text_block.dart index 36c7f6ca8..01a4208e4 100644 --- a/lib/src/editor/widgets/text/text_block.dart +++ b/lib/src/editor/widgets/text/text_block.dart @@ -209,7 +209,7 @@ class EditableTextBlock extends StatelessWidget { MediaQuery.devicePixelRatioOf(context), cursorCont, styles!.inlineCode!, - ); + null); final nodeTextDirection = getDirectionOfNode(line, textDirection); children.add( Directionality( diff --git a/lib/src/editor/widgets/text/text_line.dart b/lib/src/editor/widgets/text/text_line.dart index 0758f08b5..32e03496b 100644 --- a/lib/src/editor/widgets/text/text_line.dart +++ b/lib/src/editor/widgets/text/text_line.dart @@ -736,6 +736,7 @@ class EditableTextLine extends RenderObjectWidget { this.devicePixelRatio, this.cursorCont, this.inlineCodeStyle, + this.decoration, {super.key}); final Line line; @@ -751,6 +752,7 @@ class EditableTextLine extends RenderObjectWidget { final double devicePixelRatio; final CursorCont cursorCont; final InlineCodeStyle inlineCodeStyle; + final BoxDecoration? decoration; @override RenderObjectElement createElement() { @@ -769,7 +771,8 @@ class EditableTextLine extends RenderObjectWidget { _getPadding(), color, cursorCont, - inlineCodeStyle); + inlineCodeStyle, + decoration); } @override @@ -785,7 +788,8 @@ class EditableTextLine extends RenderObjectWidget { ..hasFocus = hasFocus ..setDevicePixelRatio(devicePixelRatio) ..setCursorCont(cursorCont) - ..setInlineCodeStyle(inlineCodeStyle); + ..setInlineCodeStyle(inlineCodeStyle) + ..setDecoration(decoration); } EdgeInsetsGeometry _getPadding() { @@ -812,6 +816,7 @@ class RenderEditableTextLine extends RenderEditableBox { this.color, this.cursorCont, this.inlineCodeStyle, + this.decoration, ); RenderBox? _leading; @@ -830,6 +835,7 @@ class RenderEditableTextLine extends RenderEditableBox { List? _selectedRects; late Rect _caretPrototype; InlineCodeStyle inlineCodeStyle; + BoxDecoration? decoration; final Map children = {}; Iterable get _children sync* { @@ -945,6 +951,12 @@ class RenderEditableTextLine extends RenderEditableBox { markNeedsLayout(); } + void setDecoration(BoxDecoration? newDecoration) { + if (decoration == newDecoration) return; + decoration = newDecoration; + markNeedsPaint(); + } + // Start selection implementation bool containsTextSelection() { @@ -1334,6 +1346,15 @@ class RenderEditableTextLine extends RenderEditableBox { ); } } + final boxDecoration = decoration; + if (boxDecoration != null) { + final paintRect = offset & size; + boxDecoration.createBoxPainter().paint( + context.canvas, + paintRect.topLeft, + ImageConfiguration(size: paintRect.size), + ); + } if (_body != null) { final parentData = _body!.parentData as BoxParentData; From 8d63996c858affde334b630b35a738c4417a620e Mon Sep 17 00:00:00 2001 From: chaos Date: Tue, 4 Mar 2025 05:36:25 +0800 Subject: [PATCH 12/40] fix: unpredictable endless loop of '_handleFocusChanged' in the phase of page route changing when editor is set to readonly. (#2488) Co-authored-by: chaos --- CHANGELOG.md | 1 + .../editor/raw_editor/raw_editor_state.dart | 22 ++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d4c095e9..88b8c29f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Remove unnecessary content change listeners in read-only mode to avoid triggering infinite loops of **FocusNode** callbacks [#2488](https://github.com/singerdmx/flutter-quill/pull/2488). - Remove unicode from `QuillText` element that causes weird caret behavior on empty lines [#2453](https://github.com/singerdmx/flutter-quill/pull/2453). - Focus and open context menu on right click if unfocused [#2477](https://github.com/singerdmx/flutter-quill/pull/2477). - Update QuillController `length` extension method deprecation message [#2483](https://github.com/singerdmx/flutter-quill/pull/2483). diff --git a/lib/src/editor/raw_editor/raw_editor_state.dart b/lib/src/editor/raw_editor/raw_editor_state.dart index e9d660505..20583d903 100644 --- a/lib/src/editor/raw_editor/raw_editor_state.dart +++ b/lib/src/editor/raw_editor/raw_editor_state.dart @@ -813,8 +813,6 @@ class QuillRawEditorState extends EditorState _clipboardStatus!.addListener(_onChangedClipboardStatus); } - controller.addListener(_didChangeTextEditingValueListener); - _scrollController = widget.config.scrollController; _scrollController.addListener(_updateSelectionOverlayForScroll); @@ -828,9 +826,6 @@ class QuillRawEditorState extends EditorState _floatingCursorResetController = AnimationController(vsync: this); _floatingCursorResetController.addListener(onFloatingCursorResetTick); - // listen to composing range changes - composingRange.addListener(_onComposingRangeChanged); - if (isKeyboardOS) { _keyboardVisible = true; } else if (!kIsWeb && isFlutterTest) { @@ -857,8 +852,13 @@ class QuillRawEditorState extends EditorState }); } - // Focus - widget.config.focusNode.addListener(_handleFocusChanged); + if (!widget.config.readOnly) { + controller.addListener(_didChangeTextEditingValueListener); + // listen to composing range changes + composingRange.addListener(_onComposingRangeChanged); + // Focus + widget.config.focusNode.addListener(_handleFocusChanged); + } } // KeyboardVisibilityController only checks for keyboards that @@ -964,10 +964,12 @@ class QuillRawEditorState extends EditorState assert(!hasConnection); _selectionOverlay?.dispose(); _selectionOverlay = null; - controller.removeListener(_didChangeTextEditingValueListener); - widget.config.focusNode.removeListener(_handleFocusChanged); + if (!widget.config.readOnly) { + controller.removeListener(_didChangeTextEditingValueListener); + widget.config.focusNode.removeListener(_handleFocusChanged); + composingRange.removeListener(_onComposingRangeChanged); + } _cursorCont.dispose(); - composingRange.removeListener(_onComposingRangeChanged); if (_clipboardStatus != null) { _clipboardStatus! ..removeListener(_onChangedClipboardStatus) From 62ecf218482a33c314f3aadaedf472bae5a77e0f Mon Sep 17 00:00:00 2001 From: Ellet Date: Tue, 11 Mar 2025 01:28:16 +0300 Subject: [PATCH 13/40] chore(release): prepare to publish 11.1.0 --- CHANGELOG.md | 5 ++++- example/pubspec.lock | 2 +- pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88b8c29f7..2347ad65a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [11.1.0] - 2025-03-11 + ### Fixed - Remove unnecessary content change listeners in read-only mode to avoid triggering infinite loops of **FocusNode** callbacks [#2488](https://github.com/singerdmx/flutter-quill/pull/2488). @@ -112,7 +114,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Apple-specific font dependency for subscript and superscript functionality from the example. - **BREAKING**: The [`super_clipboard`](https://pub.dev/packages/super_clipboard) plugin, To restore legacy behavior for `super_clipboard`, use [`flutter_quill_extensions`](https://pub.dev/packages/flutter_quill_extensions) package and `FlutterQuillExtensions.useSuperClipboardPlugin()`. -[unreleased]: https://github.com/singerdmx/flutter-quill/compare/v11.0.0...HEAD +[unreleased]: https://github.com/singerdmx/flutter-quill/compare/v11.1.0...HEAD +[11.1.0]: https://github.com/singerdmx/flutter-quill/compare/v10.0.0...v11.1.0 [11.0.0]: https://github.com/singerdmx/flutter-quill/compare/v10.0.0...v11.0.0 [10.8.5]: https://github.com/singerdmx/flutter-quill/compare/v9.4.0...v10.8.5 [9.4.0]: https://github.com/singerdmx/flutter-quill/releases/tag/v9.4.0 diff --git a/example/pubspec.lock b/example/pubspec.lock index 4c9b4098b..872b8827b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -230,7 +230,7 @@ packages: path: ".." relative: true source: path - version: "11.0.0" + version: "11.1.0" flutter_quill_delta_from_html: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8c6bc6a71..e4eb5a89e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: "A rich text editor built for Android, iOS, Web, and desktop platforms. It's the WYSIWYG editor and a Quill component for Flutter." -version: 11.0.0 +version: 11.1.0 homepage: https://github.com/singerdmx/flutter-quill/ repository: https://github.com/singerdmx/flutter-quill/ issue_tracker: https://github.com/singerdmx/flutter-quill/issues/ From 24e1305cc9847ffe891c8413d075e89b06f2079f Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Tue, 11 Mar 2025 20:29:10 -0400 Subject: [PATCH 14/40] Chore: added change to CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2347ad65a..07b63bc31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Improved support for soft-keyboards on the `TextInputClient` [#2509](https://github.com/singerdmx/flutter-quill/pull/2509) + ## [11.1.0] - 2025-03-11 ### Fixed From 90001194ee63a4b14ef1917c3e04bf62a9afef4b Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Tue, 11 Mar 2025 20:38:32 -0400 Subject: [PATCH 15/40] Chore: minor changes --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07b63bc31..610f03935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Improved support for soft-keyboards on the `TextInputClient` [#2509](https://github.com/singerdmx/flutter-quill/pull/2509) +- Improved support for soft-keyboards on the `TextInputClient` [#2509](https://github.com/singerdmx/flutter-quill/pull/2509). ## [11.1.0] - 2025-03-11 From 151898751d0af982a954657ae098615d2b9cfed1 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Tue, 11 Mar 2025 20:48:49 -0400 Subject: [PATCH 16/40] Chore: removed debounce timer since it does not needed in our implementation --- .../raw_editor/input/debounce/debounce.dart | 40 ------ .../raw_editor_state_input_client_mixin.dart | 121 ++++++------------ 2 files changed, 40 insertions(+), 121 deletions(-) delete mode 100644 lib/src/editor/raw_editor/input/debounce/debounce.dart diff --git a/lib/src/editor/raw_editor/input/debounce/debounce.dart b/lib/src/editor/raw_editor/input/debounce/debounce.dart deleted file mode 100644 index 0c905e1c8..000000000 --- a/lib/src/editor/raw_editor/input/debounce/debounce.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'dart:async'; -import 'dart:ui'; - -class Debounce { - static final Map _actions = {}; - - static void debounce( - String key, - Duration duration, - VoidCallback callback, - ) { - if (duration == Duration.zero) { - // Call immediately - callback(); - cancel(key); - } else { - cancel(key); - _actions[key] = Timer( - duration, - () { - callback(); - cancel(key); - }, - ); - } - } - - static void cancel(String key) { - _actions[key]?.cancel(); - _actions.remove(key); - } - - static void clear() { - _actions - ..forEach((key, timer) { - timer.cancel(); - }) - ..clear(); - } -} diff --git a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart index 008dc7a22..5050c843b 100644 --- a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart @@ -8,7 +8,6 @@ import 'package:flutter/material.dart' show Theme; import 'package:flutter/scheduler.dart' show SchedulerBinding; import 'package:flutter/services.dart'; import '../raw_editor.dart'; -import 'debounce/debounce.dart'; import 'diff_services.dart'; import 'formatters/text_editing_delta_formatters.dart'; import 'ime/on_delete.dart'; @@ -16,8 +15,7 @@ import 'ime/on_insert.dart'; import 'ime/on_non_update_text.dart'; import 'ime/on_replace_method.dart'; -mixin RawEditorStateTextInputClientMixin on EditorState - implements TextInputClient { +mixin RawEditorStateTextInputClientMixin on EditorState implements TextInputClient { TextInputConnection? _textInputConnection; TextEditingValue? __lastKnownRemoteTextEditingValue; @@ -28,8 +26,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState } } - TextEditingValue? get _lastKnownRemoteTextEditingValue => - __lastKnownRemoteTextEditingValue; + TextEditingValue? get _lastKnownRemoteTextEditingValue => __lastKnownRemoteTextEditingValue; /// The range of text that is currently being composed. final ValueNotifier composingRange = ValueNotifier( @@ -52,14 +49,12 @@ mixin RawEditorStateTextInputClientMixin on EditorState bool get shouldCreateInputConnection => kIsWeb || !widget.config.readOnly; /// Returns `true` if there is open input connection. - bool get hasConnection => - _textInputConnection != null && _textInputConnection!.attached; + bool get hasConnection => _textInputConnection != null && _textInputConnection!.attached; /// Opens or closes input connection based on the current state of /// [focusNode] and [value]. void openOrCloseConnection() { - if (widget.config.focusNode.hasFocus && - widget.config.focusNode.consumeKeyboardToken()) { + if (widget.config.focusNode.hasFocus && widget.config.focusNode.consumeKeyboardToken()) { openConnectionIfNeeded(); } else if (!widget.config.focusNode.hasFocus) { closeConnectionIfNeeded(); @@ -100,12 +95,9 @@ mixin RawEditorStateTextInputClientMixin on EditorState if (_lastKnownRemoteTextEditingValue != null) { if (_lastKnownRemoteTextEditingValue!.selection.end > _lastKnownRemoteTextEditingValue!.text.length) { - _lastKnownRemoteTextEditingValue = _lastKnownRemoteTextEditingValue! - .copyWith( - selection: _lastKnownRemoteTextEditingValue!.selection - .copyWith( - extentOffset: - _lastKnownRemoteTextEditingValue!.text.length)); + _lastKnownRemoteTextEditingValue = _lastKnownRemoteTextEditingValue!.copyWith( + selection: _lastKnownRemoteTextEditingValue!.selection + .copyWith(extentOffset: _lastKnownRemoteTextEditingValue!.text.length)); } } _textInputConnection!.setEditingState(_lastKnownRemoteTextEditingValue!); @@ -115,14 +107,13 @@ mixin RawEditorStateTextInputClientMixin on EditorState // windows void _updateComposingRectIfNeeded() { - final composingRange = _lastKnownRemoteTextEditingValue?.composing ?? - textEditingValue.composing; + final composingRange = + _lastKnownRemoteTextEditingValue?.composing ?? textEditingValue.composing; if (hasConnection) { assert(mounted); if (composingRange.isValid) { final offset = composingRange.start; - final composingRect = - renderEditor.getLocalRectForCaret(TextPosition(offset: offset)); + final composingRect = renderEditor.getLocalRectForCaret(TextPosition(offset: offset)); _textInputConnection!.setComposingRect(composingRect); } //SchedulerBinding.instance.addPostFrameCallback((_) => _updateComposingRectIfNeeded()); @@ -132,13 +123,9 @@ mixin RawEditorStateTextInputClientMixin on EditorState // macos void _updateCaretRectIfNeeded() { if (hasConnection) { - if (!dirty && - renderEditor.selection.isValid && - renderEditor.selection.isCollapsed) { - final currentTextPosition = - TextPosition(offset: renderEditor.selection.baseOffset); - final caretRect = - renderEditor.getLocalRectForCaret(currentTextPosition); + if (!dirty && renderEditor.selection.isValid && renderEditor.selection.isCollapsed) { + final currentTextPosition = TextPosition(offset: renderEditor.selection.baseOffset); + final caretRect = renderEditor.getLocalRectForCaret(currentTextPosition); _textInputConnection!.setCaretRect(caretRect); } //SchedulerBinding.instance.addPostFrameCallback((_) => _updateCaretRectIfNeeded()); @@ -192,8 +179,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState // Start TextInputClient implementation @override - TextEditingValue? get currentTextEditingValue => - _lastKnownRemoteTextEditingValue; + TextEditingValue? get currentTextEditingValue => _lastKnownRemoteTextEditingValue; // autofill is not needed @override @@ -224,19 +210,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState final textEditingDlta = getTextEditingDelta(currentTextEditingValue, value); _lastKnownRemoteTextEditingValue = value; - // On mobile, the IME will send a lot of updateEditingValue events, so we - // need to debounce it to combine them together. - Debounce.debounce( - 'input', - Platform.isAndroid || Platform.isIOS - ? const Duration( - milliseconds: 10, - ) - : Duration.zero, - () { - _apply([textEditingDlta]); - }, - ); + _apply([textEditingDlta]); } Future _apply(List deltas) async { @@ -278,13 +252,12 @@ mixin RawEditorStateTextInputClientMixin on EditorState if (delta is TextEditingDeltaNonTextUpdate) { composingRange.value = delta.composing; } else { - composingRange.value = - composingRange.value.start != -1 && delta.composing.end != -1 - ? TextRange( - start: composingRange.value.start, - end: delta.composing.end, - ) - : delta.composing; + composingRange.value = composingRange.value.start != -1 && delta.composing.end != -1 + ? TextRange( + start: composingRange.value.start, + end: delta.composing.end, + ) + : delta.composing; } // solve an issue where the Chinese @@ -340,49 +313,39 @@ mixin RawEditorStateTextInputClientMixin on EditorState // we cache the position. _pointOffsetOrigin = point.offset; - final currentTextPosition = - TextPosition(offset: renderEditor.selection.baseOffset); - _startCaretRect = - renderEditor.getLocalRectForCaret(currentTextPosition); + final currentTextPosition = TextPosition(offset: renderEditor.selection.baseOffset); + _startCaretRect = renderEditor.getLocalRectForCaret(currentTextPosition); - _lastBoundedOffset = _startCaretRect!.center - - _floatingCursorOffset(currentTextPosition); + _lastBoundedOffset = _startCaretRect!.center - _floatingCursorOffset(currentTextPosition); _lastTextPosition = currentTextPosition; - renderEditor.setFloatingCursor( - point.state, _lastBoundedOffset!, _lastTextPosition!); + renderEditor.setFloatingCursor(point.state, _lastBoundedOffset!, _lastTextPosition!); break; case FloatingCursorDragState.Update: assert(_lastTextPosition != null, 'Last text position was not set'); final floatingCursorOffset = _floatingCursorOffset(_lastTextPosition!); final centeredPoint = point.offset! - _pointOffsetOrigin!; - final rawCursorOffset = - _startCaretRect!.center + centeredPoint - floatingCursorOffset; + final rawCursorOffset = _startCaretRect!.center + centeredPoint - floatingCursorOffset; - final preferredLineHeight = - renderEditor.preferredLineHeight(_lastTextPosition!); + final preferredLineHeight = renderEditor.preferredLineHeight(_lastTextPosition!); _lastBoundedOffset = renderEditor.calculateBoundedFloatingCursorOffset( rawCursorOffset, preferredLineHeight, ); - _lastTextPosition = renderEditor.getPositionForOffset(renderEditor - .localToGlobal(_lastBoundedOffset! + floatingCursorOffset)); - renderEditor.setFloatingCursor( - point.state, _lastBoundedOffset!, _lastTextPosition!); + _lastTextPosition = renderEditor.getPositionForOffset( + renderEditor.localToGlobal(_lastBoundedOffset! + floatingCursorOffset)); + renderEditor.setFloatingCursor(point.state, _lastBoundedOffset!, _lastTextPosition!); final newSelection = TextSelection.collapsed( - offset: _lastTextPosition!.offset, - affinity: _lastTextPosition!.affinity); + offset: _lastTextPosition!.offset, affinity: _lastTextPosition!.affinity); // Setting selection as floating cursor moves will have scroll view // bring background cursor into view - renderEditor.onSelectionChanged( - newSelection, SelectionChangedCause.forcePress); + renderEditor.onSelectionChanged(newSelection, SelectionChangedCause.forcePress); break; case FloatingCursorDragState.End: // We skip animation if no update has happened. if (_lastTextPosition != null && _lastBoundedOffset != null) { floatingCursorResetController ..value = 0.0 - ..animateTo(1, - duration: _floatingCursorResetTime, curve: Curves.decelerate); + ..animateTo(1, duration: _floatingCursorResetTime, curve: Curves.decelerate); } break; } @@ -395,9 +358,8 @@ mixin RawEditorStateTextInputClientMixin on EditorState /// and repositioned (linear interpolation between position of floating cursor /// and current position of background cursor) void onFloatingCursorResetTick() { - final finalPosition = - renderEditor.getLocalRectForCaret(_lastTextPosition!).centerLeft - - _floatingCursorOffset(_lastTextPosition!); + final finalPosition = renderEditor.getLocalRectForCaret(_lastTextPosition!).centerLeft - + _floatingCursorOffset(_lastTextPosition!); if (floatingCursorResetController.isCompleted) { renderEditor.setFloatingCursor( FloatingCursorDragState.End, finalPosition, _lastTextPosition!); @@ -407,13 +369,11 @@ mixin RawEditorStateTextInputClientMixin on EditorState _lastBoundedOffset = null; } else { final lerpValue = floatingCursorResetController.value; - final lerpX = - lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!; - final lerpY = - lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!; + final lerpX = lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!; + final lerpY = lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!; - renderEditor.setFloatingCursor(FloatingCursorDragState.Update, - Offset(lerpX, lerpY), _lastTextPosition!, + renderEditor.setFloatingCursor( + FloatingCursorDragState.Update, Offset(lerpX, lerpY), _lastTextPosition!, resetLerpValue: lerpValue); } } @@ -441,8 +401,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState final size = renderEditor.size; final transform = renderEditor.getTransformTo(null); _textInputConnection?.setEditableSizeAndTransform(size, transform); - SchedulerBinding.instance - .addPostFrameCallback((_) => _updateSizeAndTransform()); + SchedulerBinding.instance.addPostFrameCallback((_) => _updateSizeAndTransform()); } } } From 50a1499368b8d039934bee996b53552be9f55923 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Tue, 11 Mar 2025 21:06:16 -0400 Subject: [PATCH 17/40] Chore: removed commented neccesary schedulers --- .../raw_editor/input/raw_editor_state_input_client_mixin.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart index 5050c843b..9bcb95523 100644 --- a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart @@ -116,7 +116,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements TextInputClie final composingRect = renderEditor.getLocalRectForCaret(TextPosition(offset: offset)); _textInputConnection!.setComposingRect(composingRect); } - //SchedulerBinding.instance.addPostFrameCallback((_) => _updateComposingRectIfNeeded()); + SchedulerBinding.instance.addPostFrameCallback((_) => _updateComposingRectIfNeeded()); } } @@ -128,7 +128,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements TextInputClie final caretRect = renderEditor.getLocalRectForCaret(currentTextPosition); _textInputConnection!.setCaretRect(caretRect); } - //SchedulerBinding.instance.addPostFrameCallback((_) => _updateCaretRectIfNeeded()); + SchedulerBinding.instance.addPostFrameCallback((_) => _updateCaretRectIfNeeded()); } } From d998e4518e41f0b2ae964639e506d1697a729494 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Tue, 11 Mar 2025 22:21:52 -0400 Subject: [PATCH 18/40] Chore: removed unnecessary _updateComposing method --- .../raw_editor_state_input_client_mixin.dart | 122 +++++++++--------- 1 file changed, 64 insertions(+), 58 deletions(-) diff --git a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart index 9bcb95523..5089a6cc5 100644 --- a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'dart:ui' show lerpDouble; import 'package:flutter/animation.dart' show Curves; @@ -15,7 +14,8 @@ import 'ime/on_insert.dart'; import 'ime/on_non_update_text.dart'; import 'ime/on_replace_method.dart'; -mixin RawEditorStateTextInputClientMixin on EditorState implements TextInputClient { +mixin RawEditorStateTextInputClientMixin on EditorState + implements TextInputClient { TextInputConnection? _textInputConnection; TextEditingValue? __lastKnownRemoteTextEditingValue; @@ -26,7 +26,8 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements TextInputClie } } - TextEditingValue? get _lastKnownRemoteTextEditingValue => __lastKnownRemoteTextEditingValue; + TextEditingValue? get _lastKnownRemoteTextEditingValue => + __lastKnownRemoteTextEditingValue; /// The range of text that is currently being composed. final ValueNotifier composingRange = ValueNotifier( @@ -49,12 +50,14 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements TextInputClie bool get shouldCreateInputConnection => kIsWeb || !widget.config.readOnly; /// Returns `true` if there is open input connection. - bool get hasConnection => _textInputConnection != null && _textInputConnection!.attached; + bool get hasConnection => + _textInputConnection != null && _textInputConnection!.attached; /// Opens or closes input connection based on the current state of /// [focusNode] and [value]. void openOrCloseConnection() { - if (widget.config.focusNode.hasFocus && widget.config.focusNode.consumeKeyboardToken()) { + if (widget.config.focusNode.hasFocus && + widget.config.focusNode.consumeKeyboardToken()) { openConnectionIfNeeded(); } else if (!widget.config.focusNode.hasFocus) { closeConnectionIfNeeded(); @@ -95,9 +98,12 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements TextInputClie if (_lastKnownRemoteTextEditingValue != null) { if (_lastKnownRemoteTextEditingValue!.selection.end > _lastKnownRemoteTextEditingValue!.text.length) { - _lastKnownRemoteTextEditingValue = _lastKnownRemoteTextEditingValue!.copyWith( - selection: _lastKnownRemoteTextEditingValue!.selection - .copyWith(extentOffset: _lastKnownRemoteTextEditingValue!.text.length)); + _lastKnownRemoteTextEditingValue = _lastKnownRemoteTextEditingValue! + .copyWith( + selection: _lastKnownRemoteTextEditingValue!.selection + .copyWith( + extentOffset: + _lastKnownRemoteTextEditingValue!.text.length)); } } _textInputConnection!.setEditingState(_lastKnownRemoteTextEditingValue!); @@ -107,28 +113,35 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements TextInputClie // windows void _updateComposingRectIfNeeded() { - final composingRange = - _lastKnownRemoteTextEditingValue?.composing ?? textEditingValue.composing; + final composingRange = _lastKnownRemoteTextEditingValue?.composing ?? + textEditingValue.composing; if (hasConnection) { assert(mounted); if (composingRange.isValid) { final offset = composingRange.start; - final composingRect = renderEditor.getLocalRectForCaret(TextPosition(offset: offset)); + final composingRect = + renderEditor.getLocalRectForCaret(TextPosition(offset: offset)); _textInputConnection!.setComposingRect(composingRect); } - SchedulerBinding.instance.addPostFrameCallback((_) => _updateComposingRectIfNeeded()); + SchedulerBinding.instance + .addPostFrameCallback((_) => _updateComposingRectIfNeeded()); } } // macos void _updateCaretRectIfNeeded() { if (hasConnection) { - if (!dirty && renderEditor.selection.isValid && renderEditor.selection.isCollapsed) { - final currentTextPosition = TextPosition(offset: renderEditor.selection.baseOffset); - final caretRect = renderEditor.getLocalRectForCaret(currentTextPosition); + if (!dirty && + renderEditor.selection.isValid && + renderEditor.selection.isCollapsed) { + final currentTextPosition = + TextPosition(offset: renderEditor.selection.baseOffset); + final caretRect = + renderEditor.getLocalRectForCaret(currentTextPosition); _textInputConnection!.setCaretRect(caretRect); } - SchedulerBinding.instance.addPostFrameCallback((_) => _updateCaretRectIfNeeded()); + SchedulerBinding.instance + .addPostFrameCallback((_) => _updateCaretRectIfNeeded()); } } @@ -179,7 +192,8 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements TextInputClie // Start TextInputClient implementation @override - TextEditingValue? get currentTextEditingValue => _lastKnownRemoteTextEditingValue; + TextEditingValue? get currentTextEditingValue => + _lastKnownRemoteTextEditingValue; // autofill is not needed @override @@ -220,8 +234,6 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements TextInputClie ) .toList(); for (final delta in formattedDeltas) { - _updateComposing(delta); - if (delta is TextEditingDeltaInsertion) { await onInsert( delta, @@ -248,26 +260,6 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements TextInputClie } } - void _updateComposing(TextEditingDelta delta) { - if (delta is TextEditingDeltaNonTextUpdate) { - composingRange.value = delta.composing; - } else { - composingRange.value = composingRange.value.start != -1 && delta.composing.end != -1 - ? TextRange( - start: composingRange.value.start, - end: delta.composing.end, - ) - : delta.composing; - } - - // solve an issue where the Chinese - // IME doesn't continue deleting after - // the input content has been deleted. - if (Platform.isMacOS && (composingRange.value.isCollapsed)) { - composingRange.value = TextRange.empty; - } - } - @override void performAction(TextInputAction action) { widget.config.onPerformAction?.call(action); @@ -313,39 +305,49 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements TextInputClie // we cache the position. _pointOffsetOrigin = point.offset; - final currentTextPosition = TextPosition(offset: renderEditor.selection.baseOffset); - _startCaretRect = renderEditor.getLocalRectForCaret(currentTextPosition); + final currentTextPosition = + TextPosition(offset: renderEditor.selection.baseOffset); + _startCaretRect = + renderEditor.getLocalRectForCaret(currentTextPosition); - _lastBoundedOffset = _startCaretRect!.center - _floatingCursorOffset(currentTextPosition); + _lastBoundedOffset = _startCaretRect!.center - + _floatingCursorOffset(currentTextPosition); _lastTextPosition = currentTextPosition; - renderEditor.setFloatingCursor(point.state, _lastBoundedOffset!, _lastTextPosition!); + renderEditor.setFloatingCursor( + point.state, _lastBoundedOffset!, _lastTextPosition!); break; case FloatingCursorDragState.Update: assert(_lastTextPosition != null, 'Last text position was not set'); final floatingCursorOffset = _floatingCursorOffset(_lastTextPosition!); final centeredPoint = point.offset! - _pointOffsetOrigin!; - final rawCursorOffset = _startCaretRect!.center + centeredPoint - floatingCursorOffset; + final rawCursorOffset = + _startCaretRect!.center + centeredPoint - floatingCursorOffset; - final preferredLineHeight = renderEditor.preferredLineHeight(_lastTextPosition!); + final preferredLineHeight = + renderEditor.preferredLineHeight(_lastTextPosition!); _lastBoundedOffset = renderEditor.calculateBoundedFloatingCursorOffset( rawCursorOffset, preferredLineHeight, ); - _lastTextPosition = renderEditor.getPositionForOffset( - renderEditor.localToGlobal(_lastBoundedOffset! + floatingCursorOffset)); - renderEditor.setFloatingCursor(point.state, _lastBoundedOffset!, _lastTextPosition!); + _lastTextPosition = renderEditor.getPositionForOffset(renderEditor + .localToGlobal(_lastBoundedOffset! + floatingCursorOffset)); + renderEditor.setFloatingCursor( + point.state, _lastBoundedOffset!, _lastTextPosition!); final newSelection = TextSelection.collapsed( - offset: _lastTextPosition!.offset, affinity: _lastTextPosition!.affinity); + offset: _lastTextPosition!.offset, + affinity: _lastTextPosition!.affinity); // Setting selection as floating cursor moves will have scroll view // bring background cursor into view - renderEditor.onSelectionChanged(newSelection, SelectionChangedCause.forcePress); + renderEditor.onSelectionChanged( + newSelection, SelectionChangedCause.forcePress); break; case FloatingCursorDragState.End: // We skip animation if no update has happened. if (_lastTextPosition != null && _lastBoundedOffset != null) { floatingCursorResetController ..value = 0.0 - ..animateTo(1, duration: _floatingCursorResetTime, curve: Curves.decelerate); + ..animateTo(1, + duration: _floatingCursorResetTime, curve: Curves.decelerate); } break; } @@ -358,8 +360,9 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements TextInputClie /// and repositioned (linear interpolation between position of floating cursor /// and current position of background cursor) void onFloatingCursorResetTick() { - final finalPosition = renderEditor.getLocalRectForCaret(_lastTextPosition!).centerLeft - - _floatingCursorOffset(_lastTextPosition!); + final finalPosition = + renderEditor.getLocalRectForCaret(_lastTextPosition!).centerLeft - + _floatingCursorOffset(_lastTextPosition!); if (floatingCursorResetController.isCompleted) { renderEditor.setFloatingCursor( FloatingCursorDragState.End, finalPosition, _lastTextPosition!); @@ -369,11 +372,13 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements TextInputClie _lastBoundedOffset = null; } else { final lerpValue = floatingCursorResetController.value; - final lerpX = lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!; - final lerpY = lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!; + final lerpX = + lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!; + final lerpY = + lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!; - renderEditor.setFloatingCursor( - FloatingCursorDragState.Update, Offset(lerpX, lerpY), _lastTextPosition!, + renderEditor.setFloatingCursor(FloatingCursorDragState.Update, + Offset(lerpX, lerpY), _lastTextPosition!, resetLerpValue: lerpValue); } } @@ -401,7 +406,8 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements TextInputClie final size = renderEditor.size; final transform = renderEditor.getTransformTo(null); _textInputConnection?.setEditableSizeAndTransform(size, transform); - SchedulerBinding.instance.addPostFrameCallback((_) => _updateSizeAndTransform()); + SchedulerBinding.instance + .addPostFrameCallback((_) => _updateSizeAndTransform()); } } } From 6ceef547aa537403c586615c21fc47a29f6852fb Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Tue, 11 Mar 2025 22:22:48 -0400 Subject: [PATCH 19/40] Fix: buggy behavior of the caret when try to delete characters on android --- lib/src/editor/raw_editor/input/ime/on_delete.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/editor/raw_editor/input/ime/on_delete.dart b/lib/src/editor/raw_editor/input/ime/on_delete.dart index 402779303..3833e6af0 100644 --- a/lib/src/editor/raw_editor/input/ime/on_delete.dart +++ b/lib/src/editor/raw_editor/input/ime/on_delete.dart @@ -14,7 +14,7 @@ Future onDelete( length, '', TextSelection.collapsed( - offset: start > 0 ? start - 1 : 0, + offset: (selection.baseOffset - 1).nonNegative, affinity: controller.selection.affinity, ), ); @@ -30,3 +30,7 @@ Future onDelete( ), ); } + +extension on int { + int get nonNegative => this < 0 ? 0 : this; +} From fd6636888e61046c1c75e46ff819dc9d2e5748c7 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Tue, 11 Mar 2025 23:00:15 -0400 Subject: [PATCH 20/40] Chore: used old implementation for web since the new one does not work as expected --- .../input/ime/on_non_update_text.dart | 2 -- .../raw_editor_state_input_client_mixin.dart | 24 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart b/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart index 16a0d04c5..157538cf5 100644 --- a/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart +++ b/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart @@ -6,8 +6,6 @@ Future onNonTextUpdate( TextEditingDeltaNonTextUpdate nonTextUpdate, QuillController controller, ) async { - // update the selection on Windows - // // when typing characters with CJK IME on Windows, a non-text update is sent // with the selection range. if (Platform.isWindows) { diff --git a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart index 5089a6cc5..d3aba4e8c 100644 --- a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart @@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart' show ValueNotifier, kIsWeb; import 'package:flutter/material.dart' show Theme; import 'package:flutter/scheduler.dart' show SchedulerBinding; import 'package:flutter/services.dart'; +import '../../../delta/delta_diff.dart'; +import '../../../document/document.dart'; import '../raw_editor.dart'; import 'diff_services.dart'; import 'formatters/text_editing_delta_formatters.dart'; @@ -222,6 +224,28 @@ mixin RawEditorStateTextInputClientMixin on EditorState return; } + if (kIsWeb) { + final effectiveLastKnownValue = _lastKnownRemoteTextEditingValue!; + _lastKnownRemoteTextEditingValue = value; + final oldText = effectiveLastKnownValue.text; + final text = value.text; + final cursorPosition = value.isComposingRangeValid + ? value.composing.end + : value.selection.extentOffset; + final diff = getDiff(oldText, text, cursorPosition); + if (diff.deleted.isEmpty && diff.inserted.isEmpty) { + widget.controller.updateSelection(value.selection, ChangeSource.local); + } else { + widget.controller.replaceText( + diff.start, + diff.deleted.length, + diff.inserted, + value.selection, + ); + } + return; + } + final textEditingDlta = getTextEditingDelta(currentTextEditingValue, value); _lastKnownRemoteTextEditingValue = value; _apply([textEditingDlta]); From e98e16d5a514fa19b8d25e8e158370cd88872673 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Wed, 12 Mar 2025 02:59:19 -0400 Subject: [PATCH 21/40] Chore: removed composing range validation to get the cursorPosition for web, since its buggy --- .../input/ime/on_non_update_text.dart | 20 ++++++++----------- .../raw_editor_state_input_client_mixin.dart | 5 ++--- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart b/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart index 157538cf5..56986301f 100644 --- a/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart +++ b/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart @@ -6,24 +6,20 @@ Future onNonTextUpdate( TextEditingDeltaNonTextUpdate nonTextUpdate, QuillController controller, ) async { + final effectiveSelection = TextSelection.collapsed(offset: nonTextUpdate.selection.baseOffset); // when typing characters with CJK IME on Windows, a non-text update is sent // with the selection range. if (Platform.isWindows) { - if (nonTextUpdate.composing == TextRange.empty && - nonTextUpdate.selection.isCollapsed) { + if (nonTextUpdate.composing == TextRange.empty && nonTextUpdate.selection.isCollapsed) { controller.updateSelection( - TextSelection.collapsed( - offset: nonTextUpdate.selection.start, - ), + effectiveSelection, ChangeSource.local, ); } - } else if (Platform.isLinux || Platform.isMacOS) { - controller.updateSelection( - TextSelection.collapsed( - offset: nonTextUpdate.selection.start, - ), - ChangeSource.local, - ); + return; } + controller.updateSelection( + effectiveSelection, + ChangeSource.local, + ); } diff --git a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart index d3aba4e8c..9c0622663 100644 --- a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart @@ -224,14 +224,13 @@ mixin RawEditorStateTextInputClientMixin on EditorState return; } + // on the web, using TextEditingDeltas not works as expected if (kIsWeb) { final effectiveLastKnownValue = _lastKnownRemoteTextEditingValue!; _lastKnownRemoteTextEditingValue = value; final oldText = effectiveLastKnownValue.text; final text = value.text; - final cursorPosition = value.isComposingRangeValid - ? value.composing.end - : value.selection.extentOffset; + final cursorPosition = value.selection.extentOffset; final diff = getDiff(oldText, text, cursorPosition); if (diff.deleted.isEmpty && diff.inserted.isEmpty) { widget.controller.updateSelection(value.selection, ChangeSource.local); From 920256033e17b239e175fdd7e32b86b9dbc73c15 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Wed, 12 Mar 2025 12:28:46 -0400 Subject: [PATCH 22/40] Chore: replaced use of dart:io to use platform utils --- lib/src/common/utils/platform.dart | 8 ++++++++ .../raw_editor/input/ime/on_non_update_text.dart | 10 ++++++---- .../editor/raw_editor/input/ime/on_replace_method.dart | 3 ++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/src/common/utils/platform.dart b/lib/src/common/utils/platform.dart index 7d673469b..516bf3d1d 100644 --- a/lib/src/common/utils/platform.dart +++ b/lib/src/common/utils/platform.dart @@ -51,6 +51,14 @@ bool get isDesktop => @pragma('vm:platform-const-if', !kDebugMode) bool get isDesktopApp => !kIsWeb && isDesktop; +// windows + +@pragma('vm:platform-const-if', !kDebugMode) +bool get isWindows => defaultTargetPlatform == TargetPlatform.windows; + +@pragma('vm:platform-const-if', !kDebugMode) +bool get isWindowsApp => !kIsWeb && isWindows; + // macOS @pragma('vm:platform-const-if', !kDebugMode) diff --git a/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart b/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart index 56986301f..d5395f8b8 100644 --- a/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart +++ b/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart @@ -1,16 +1,18 @@ -import 'dart:io'; import 'package:flutter/services.dart'; import '../../../../../flutter_quill.dart'; +import '../../../../../internal.dart'; Future onNonTextUpdate( TextEditingDeltaNonTextUpdate nonTextUpdate, QuillController controller, ) async { - final effectiveSelection = TextSelection.collapsed(offset: nonTextUpdate.selection.baseOffset); + final effectiveSelection = + TextSelection.collapsed(offset: nonTextUpdate.selection.baseOffset); // when typing characters with CJK IME on Windows, a non-text update is sent // with the selection range. - if (Platform.isWindows) { - if (nonTextUpdate.composing == TextRange.empty && nonTextUpdate.selection.isCollapsed) { + if (isWindowsApp) { + if (nonTextUpdate.composing == TextRange.empty && + nonTextUpdate.selection.isCollapsed) { controller.updateSelection( effectiveSelection, ChangeSource.local, diff --git a/lib/src/editor/raw_editor/input/ime/on_replace_method.dart b/lib/src/editor/raw_editor/input/ime/on_replace_method.dart index 867b8cbff..b667ed10d 100644 --- a/lib/src/editor/raw_editor/input/ime/on_replace_method.dart +++ b/lib/src/editor/raw_editor/input/ime/on_replace_method.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/services.dart'; +import '../../../../../internal.dart'; import '../../../../controller/quill_controller.dart'; import '../../../raw_editor/config/events/character_shortcuts_events.dart'; import 'on_insert.dart'; @@ -26,7 +27,7 @@ Future onReplace( } } - if (Platform.isIOS) { + if (isIosApp) { // remove the trailing '\n' when pressing the return key if (textReplacement.endsWith('\n')) { replacement = TextEditingDeltaReplacement( From 25d07adaebcfb98b3048a65c271284ab75c049fb Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Wed, 12 Mar 2025 15:20:05 -0400 Subject: [PATCH 23/40] Chore: fix buggy behavior in onDelete on web browsers --- .../raw_editor/input/diff_services.dart | 27 ++++++-------- .../raw_editor/input/ime/on_delete.dart | 25 +++---------- .../raw_editor_state_input_client_mixin.dart | 37 +++++-------------- 3 files changed, 28 insertions(+), 61 deletions(-) diff --git a/lib/src/editor/raw_editor/input/diff_services.dart b/lib/src/editor/raw_editor/input/diff_services.dart index f2d474088..efea823a5 100644 --- a/lib/src/editor/raw_editor/input/diff_services.dart +++ b/lib/src/editor/raw_editor/input/diff_services.dart @@ -3,23 +3,26 @@ import '../../../delta/delta_diff.dart'; /// Return a list of the change type that was do it to the content of the editor TextEditingDelta getTextEditingDelta( - TextEditingValue? oldValue, + TextEditingValue oldValue, TextEditingValue newValue, ) { - if (oldValue == null || oldValue.text == newValue.text) { - return TextEditingDeltaNonTextUpdate( - oldText: newValue.text, - selection: newValue.selection, - composing: newValue.composing, - ); - } + // we need to check why sometimes in android, when we place the caret + // at a position, it moves backward unexpectly. By now, i think that we need to use + // the removed Debounce class to wait for the android soft-keyboard events + // since on android, non-text-update is called more times that we think final currentText = oldValue.text; final diff = getDiff( currentText, newValue.text, newValue.selection.extentOffset, ); - if (diff.inserted.isNotEmpty && diff.deleted.isEmpty) { + if (diff.inserted.isEmpty && diff.deleted.isEmpty) { + return TextEditingDeltaNonTextUpdate( + oldText: newValue.text, + selection: newValue.selection, + composing: newValue.composing, + ); + } else if (diff.inserted.isNotEmpty && diff.deleted.isEmpty) { return TextEditingDeltaInsertion( oldText: currentText, textInserted: diff.inserted, @@ -48,12 +51,6 @@ TextEditingDelta getTextEditingDelta( end: diff.start + diff.deleted.length, ), ); - } else if (diff.inserted.isEmpty && diff.deleted.isEmpty) { - return TextEditingDeltaNonTextUpdate( - oldText: newValue.text, - selection: newValue.selection, - composing: newValue.composing, - ); } throw UnsupportedError('Unknown diff: $diff'); } diff --git a/lib/src/editor/raw_editor/input/ime/on_delete.dart b/lib/src/editor/raw_editor/input/ime/on_delete.dart index 3833e6af0..2632e633c 100644 --- a/lib/src/editor/raw_editor/input/ime/on_delete.dart +++ b/lib/src/editor/raw_editor/input/ime/on_delete.dart @@ -5,28 +5,15 @@ Future onDelete( TextEditingDeltaDeletion deletion, QuillController controller, ) async { - final selection = controller.selection; - if (selection.isCollapsed) { - final start = deletion.deletedRange.start; - final length = deletion.deletedRange.end - start; - controller.replaceText( - start + 1, - length, - '', - TextSelection.collapsed( - offset: (selection.baseOffset - 1).nonNegative, - affinity: controller.selection.affinity, - ), - ); - return; - } + final start = deletion.deletedRange.start; + final length = deletion.deletedRange.end - start; controller.replaceText( - selection.baseOffset, - selection.extentOffset - selection.baseOffset, + start, + length, '', TextSelection.collapsed( - offset: selection.start, - affinity: selection.affinity, + offset: deletion.selection.baseOffset.nonNegative, + affinity: controller.selection.affinity, ), ); } diff --git a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart index 9c0622663..47489a4bc 100644 --- a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart @@ -224,38 +224,21 @@ mixin RawEditorStateTextInputClientMixin on EditorState return; } - // on the web, using TextEditingDeltas not works as expected - if (kIsWeb) { - final effectiveLastKnownValue = _lastKnownRemoteTextEditingValue!; - _lastKnownRemoteTextEditingValue = value; - final oldText = effectiveLastKnownValue.text; - final text = value.text; - final cursorPosition = value.selection.extentOffset; - final diff = getDiff(oldText, text, cursorPosition); - if (diff.deleted.isEmpty && diff.inserted.isEmpty) { - widget.controller.updateSelection(value.selection, ChangeSource.local); - } else { - widget.controller.replaceText( - diff.start, - diff.deleted.length, - diff.inserted, - value.selection, - ); - } - return; - } - - final textEditingDlta = getTextEditingDelta(currentTextEditingValue, value); + final textEditingDlta = + getTextEditingDelta(_lastKnownRemoteTextEditingValue!, value); _lastKnownRemoteTextEditingValue = value; _apply([textEditingDlta]); } Future _apply(List deltas) async { - final formattedDeltas = deltas - .map( - (e) => e.format(), - ) - .toList(); + // on web browsers, we don't need to format the deltas + final formattedDeltas = kIsWeb + ? deltas + : deltas + .map( + (e) => e.format(), + ) + .toList(); for (final delta in formattedDeltas) { if (delta is TextEditingDeltaInsertion) { await onInsert( From 9ff127dd56e87b11a82979558e5b3ea224d83a6b Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Wed, 12 Mar 2025 18:48:38 -0400 Subject: [PATCH 24/40] Chore: removed formatters since them are not doing nothing and cause more issues --- .../text_editing_delta_formatters.dart | 130 ------------------ 1 file changed, 130 deletions(-) delete mode 100644 lib/src/editor/raw_editor/input/formatters/text_editing_delta_formatters.dart diff --git a/lib/src/editor/raw_editor/input/formatters/text_editing_delta_formatters.dart b/lib/src/editor/raw_editor/input/formatters/text_editing_delta_formatters.dart deleted file mode 100644 index f3147f5e4..000000000 --- a/lib/src/editor/raw_editor/input/formatters/text_editing_delta_formatters.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'dart:math'; -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart'; - -const String _whitespace = ' '; -const int _whitespaceLen = _whitespace.length; - -// Extension on TextEditingDelta to provide a generic formatting method. -// This method checks the type of the TextEditingDelta and calls the appropriate -// formatting method for the specific delta type (insertion, deletion, replacement, or non-text update). -// If the delta type is not recognized, it throws an UnimplementedError. -@internal -@experimental -extension GeneralTextEditingFormatter on TextEditingDelta { - TextEditingDelta format() { - if (this is TextEditingDeltaInsertion) { - return (this as TextEditingDeltaInsertion).format(); - } else if (this is TextEditingDeltaDeletion) { - return (this as TextEditingDeltaDeletion).format(); - } else if (this is TextEditingDeltaReplacement) { - return (this as TextEditingDeltaReplacement).format(); - } else if (this is TextEditingDeltaNonTextUpdate) { - return (this as TextEditingDeltaNonTextUpdate).format(); - } - throw UnimplementedError(); - } -} - -// Extension on TextEditingDeltaInsertion to format insertion deltas. -// Adjusts the oldText, insertionOffset, selection, and composing properties -// by shifting them based on a predefined whitespace length. -@internal -@experimental -extension TextInsertionFormatter on TextEditingDeltaInsertion { - TextEditingDeltaInsertion format() => TextEditingDeltaInsertion( - oldText: oldText << _whitespaceLen, - textInserted: textInserted, - insertionOffset: insertionOffset - _whitespaceLen, - selection: selection << _whitespaceLen, - composing: composing << _whitespaceLen, - ); -} - -// Extension on TextEditingDeltaDeletion to format deletion deltas. -// Adjusts the oldText, deletedRange, selection, and composing properties -// by shifting them based on a predefined whitespace length. -@internal -@experimental -extension TextDeletionFormatter on TextEditingDeltaDeletion { - TextEditingDeltaDeletion format() => TextEditingDeltaDeletion( - oldText: oldText << _whitespaceLen, - deletedRange: deletedRange << _whitespaceLen, - selection: selection << _whitespaceLen, - composing: composing << _whitespaceLen, - ); -} - -// Extension on TextEditingDeltaReplacement to format replacement deltas. -// Adjusts the oldText, replacedRange, selection, and composing properties -// by shifting them based on a predefined whitespace length. -@internal -@experimental -extension TextReplacementFormatter on TextEditingDeltaReplacement { - TextEditingDeltaReplacement format() => TextEditingDeltaReplacement( - oldText: oldText << _whitespaceLen, - replacementText: replacementText, - replacedRange: replacedRange << _whitespaceLen, - selection: selection << _whitespaceLen, - composing: composing << _whitespaceLen, - ); -} - -// Extension on TextEditingDeltaNonTextUpdate to format non-text update deltas. -// Adjusts the oldText, selection, and composing properties -// by shifting them based on a predefined whitespace length. -@internal -@experimental -extension NonTextUpdateFormatter on TextEditingDeltaNonTextUpdate { - TextEditingDeltaNonTextUpdate format() => TextEditingDeltaNonTextUpdate( - oldText: oldText << _whitespaceLen, - selection: selection << _whitespaceLen, - composing: composing << _whitespaceLen, - ); -} - -// Extension on TextRange to provide shifting functionality. -// Allows shifting the start and end positions of a TextRange by a specified amount. -// If the range is invalid, it returns the original range. -@internal -@experimental -extension ShiftTextRange on TextRange { - TextRange operator <<(int shiftAmount) => shift(-shiftAmount); - - TextRange shift(int shiftAmount) => !isValid - ? this - : TextRange( - start: max(0, start + shiftAmount), - end: max(0, end + shiftAmount), - ); -} - -// Extension on String to provide shifting functionality. -// Allows shifting the string by removing a specified number of characters from the beginning. -// If the shift amount is greater than the string length, it returns an empty string. -@internal -@experimental -extension ShiftString on String { - String operator <<(int shiftAmount) => shift(shiftAmount); - - String shift(int shiftAmount) { - if (shiftAmount > length) { - return ''; - } - return substring(shiftAmount); - } -} - -// Extension on TextSelection to provide shifting functionality. -// Allows shifting the baseOffset and extentOffset of a TextSelection by a specified amount. -// Ensures the offsets do not go below zero. -@internal -@experimental -extension ShiftTextSelection on TextSelection { - TextSelection operator <<(int shiftAmount) => shift(-shiftAmount); - - TextSelection shift(int shiftAmount) => TextSelection( - baseOffset: max(0, baseOffset + shiftAmount), - extentOffset: max(0, extentOffset + shiftAmount), - ); -} From 70ccec12a36224ef4a9b8007361c39dafda202c7 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Wed, 12 Mar 2025 18:49:35 -0400 Subject: [PATCH 25/40] Chore: replaced get selection from controller to use the one from the new state of the TextEditingValue --- .../raw_editor/input/ime/on_insert.dart | 9 ++--- .../input/ime/on_replace_method.dart | 36 ++++++++----------- .../raw_editor_state_input_client_mixin.dart | 10 +----- 3 files changed, 21 insertions(+), 34 deletions(-) diff --git a/lib/src/editor/raw_editor/input/ime/on_insert.dart b/lib/src/editor/raw_editor/input/ime/on_insert.dart index 88f8d6745..d607f4d96 100644 --- a/lib/src/editor/raw_editor/input/ime/on_insert.dart +++ b/lib/src/editor/raw_editor/input/ime/on_insert.dart @@ -8,14 +8,13 @@ Future onInsert( QuillController controller, List characterShortcutEvents, ) async { - final selection = controller.selection; + final selection = insertion.selection; final insertionText = insertion.textInserted; if (insertionText.length == 1 && !insertionText.contains('\n')) { for (final shortcutEvent in characterShortcutEvents) { - if (shortcutEvent.character == insertionText && - shortcutEvent.handler(controller)) { + if (shortcutEvent.character == insertionText && shortcutEvent.handler(controller)) { return; } } @@ -26,6 +25,8 @@ Future onInsert( selection.extentOffset - selection.baseOffset, insertionText, TextSelection.collapsed( - offset: selection.extentOffset + insertionText.length), + offset: insertion.selection.baseOffset + insertionText.length, + affinity: insertion.selection.affinity, + ), ); } diff --git a/lib/src/editor/raw_editor/input/ime/on_replace_method.dart b/lib/src/editor/raw_editor/input/ime/on_replace_method.dart index b667ed10d..cdd63c951 100644 --- a/lib/src/editor/raw_editor/input/ime/on_replace_method.dart +++ b/lib/src/editor/raw_editor/input/ime/on_replace_method.dart @@ -13,15 +13,14 @@ Future onReplace( List characterShortcutEvents, ) async { // delete the selection - final selection = controller.selection; + final selection = replacement.selection; final textReplacement = replacement.replacementText; if (selection.isCollapsed) { if (textReplacement.length == 1) { for (final shortcutEvent in characterShortcutEvents) { - if (shortcutEvent.character == textReplacement && - shortcutEvent.handler(controller)) { + if (shortcutEvent.character == textReplacement && shortcutEvent.handler(controller)) { return; } } @@ -32,8 +31,10 @@ Future onReplace( if (textReplacement.endsWith('\n')) { replacement = TextEditingDeltaReplacement( oldText: replacement.oldText, - replacementText: replacement.replacementText - .substring(0, replacement.replacementText.length - 1), + replacementText: replacement.replacementText.substring( + 0, + replacement.replacementText.length - 1, + ), replacedRange: replacement.replacedRange, selection: replacement.selection, composing: replacement.composing, @@ -41,31 +42,24 @@ Future onReplace( } } + final insertion = replacement.toInsertion(); + await onInsert( + insertion, + controller, + characterShortcutEvents, + ); + } else { final start = replacement.replacedRange.start; final length = replacement.replacedRange.end - start; controller.replaceText( start, length, - textReplacement, - TextSelection.collapsed( - offset: replacement.selection.baseOffset + textReplacement.length), - ); - } else { - controller.replaceText( - selection.baseOffset, - selection.extentOffset - selection.baseOffset, - '', + replacement.replacementText, TextSelection.collapsed( offset: selection.baseOffset, + affinity: selection.affinity, ), ); - // insert the replacement - final insertion = replacement.toInsertion(); - await onInsert( - insertion, - controller, - characterShortcutEvents, - ); } } diff --git a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart index 47489a4bc..2d90d9622 100644 --- a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart @@ -231,15 +231,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState } Future _apply(List deltas) async { - // on web browsers, we don't need to format the deltas - final formattedDeltas = kIsWeb - ? deltas - : deltas - .map( - (e) => e.format(), - ) - .toList(); - for (final delta in formattedDeltas) { + for (final delta in deltas) { if (delta is TextEditingDeltaInsertion) { await onInsert( delta, From b5ee9f451919d28511077f1eccba366347a2a0a0 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Wed, 12 Mar 2025 18:50:14 -0400 Subject: [PATCH 26/40] Chore: removed unused import --- lib/src/editor/raw_editor/input/ime/on_replace_method.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/src/editor/raw_editor/input/ime/on_replace_method.dart b/lib/src/editor/raw_editor/input/ime/on_replace_method.dart index cdd63c951..3ca7116f0 100644 --- a/lib/src/editor/raw_editor/input/ime/on_replace_method.dart +++ b/lib/src/editor/raw_editor/input/ime/on_replace_method.dart @@ -1,7 +1,4 @@ -import 'dart:io'; - import 'package:flutter/services.dart'; - import '../../../../../internal.dart'; import '../../../../controller/quill_controller.dart'; import '../../../raw_editor/config/events/character_shortcuts_events.dart'; From aad0b56ef97ce01fd07e59fef8ff9b640b6fe372 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Wed, 12 Mar 2025 18:52:47 -0400 Subject: [PATCH 27/40] Fix: removed unused imports and deleted import for formatters --- .../raw_editor/input/raw_editor_state_input_client_mixin.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart index 2d90d9622..074abefa5 100644 --- a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart @@ -6,11 +6,8 @@ import 'package:flutter/foundation.dart' show ValueNotifier, kIsWeb; import 'package:flutter/material.dart' show Theme; import 'package:flutter/scheduler.dart' show SchedulerBinding; import 'package:flutter/services.dart'; -import '../../../delta/delta_diff.dart'; -import '../../../document/document.dart'; import '../raw_editor.dart'; import 'diff_services.dart'; -import 'formatters/text_editing_delta_formatters.dart'; import 'ime/on_delete.dart'; import 'ime/on_insert.dart'; import 'ime/on_non_update_text.dart'; From ea112f1bfd8ece549552a6cbf47351af88db188d Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Wed, 12 Mar 2025 19:39:23 -0400 Subject: [PATCH 28/40] Chore: removed unnecessary double check for use CharacterShortcutEvet in onReplace method --- .../editor/raw_editor/input/ime/on_replace_method.dart | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/src/editor/raw_editor/input/ime/on_replace_method.dart b/lib/src/editor/raw_editor/input/ime/on_replace_method.dart index 3ca7116f0..ee7508a73 100644 --- a/lib/src/editor/raw_editor/input/ime/on_replace_method.dart +++ b/lib/src/editor/raw_editor/input/ime/on_replace_method.dart @@ -15,14 +15,6 @@ Future onReplace( final textReplacement = replacement.replacementText; if (selection.isCollapsed) { - if (textReplacement.length == 1) { - for (final shortcutEvent in characterShortcutEvents) { - if (shortcutEvent.character == textReplacement && shortcutEvent.handler(controller)) { - return; - } - } - } - if (isIosApp) { // remove the trailing '\n' when pressing the return key if (textReplacement.endsWith('\n')) { From fa3dbc32b19a0fd019b1543744e0763ac7255fab Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Wed, 12 Mar 2025 19:42:15 -0400 Subject: [PATCH 29/40] Chore: updated change title in CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 610f03935..27a558738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Improved support for soft-keyboards on the `TextInputClient` [#2509](https://github.com/singerdmx/flutter-quill/pull/2509). +- Improve changes application from TextInputClient [#2509](https://github.com/singerdmx/flutter-quill/pull/2509). ## [11.1.0] - 2025-03-11 From f12327b59201f833543640574a297777d64384be Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Wed, 12 Mar 2025 21:34:01 -0400 Subject: [PATCH 30/40] Chore: update pubspec.lock from example --- example/pubspec.lock | 86 ++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 872b8827b..22f9e3177 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -13,26 +13,26 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" charcode: dependency: transitive description: @@ -45,18 +45,18 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" cross_file: dependency: transitive description: @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" ffi: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" file_selector_linux: dependency: transitive description: @@ -373,18 +373,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -413,10 +413,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -429,10 +429,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: @@ -445,10 +445,10 @@ packages: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" photo_view: dependency: transitive description: @@ -461,10 +461,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -477,10 +477,10 @@ packages: dependency: transitive description: name: process - sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "5.0.3" quill_native_bridge: dependency: transitive description: @@ -562,34 +562,34 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" sync_http: dependency: transitive description: @@ -602,18 +602,18 @@ packages: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" typed_data: dependency: transitive description: @@ -738,10 +738,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "14.3.1" web: dependency: transitive description: @@ -767,5 +767,5 @@ packages: source: hosted version: "5.10.1" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.27.0" From b1fbe0a4af6a94a157bd2e19b66739d33d4321b0 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Wed, 12 Mar 2025 22:01:59 -0400 Subject: [PATCH 31/40] Chore: use insertion.insertionOffset instead selection from new value --- lib/src/editor/raw_editor/input/ime/on_insert.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/editor/raw_editor/input/ime/on_insert.dart b/lib/src/editor/raw_editor/input/ime/on_insert.dart index d607f4d96..43f3cb538 100644 --- a/lib/src/editor/raw_editor/input/ime/on_insert.dart +++ b/lib/src/editor/raw_editor/input/ime/on_insert.dart @@ -21,12 +21,12 @@ Future onInsert( } controller.replaceText( - selection.baseOffset, + insertion.insertionOffset, selection.extentOffset - selection.baseOffset, insertionText, TextSelection.collapsed( - offset: insertion.selection.baseOffset + insertionText.length, - affinity: insertion.selection.affinity, + offset: insertion.insertionOffset + insertionText.length, + affinity: selection.affinity, ), ); } From f59c58bce3376b857463d6ca18fe030193075ca8 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Thu, 13 Mar 2025 00:06:00 -0400 Subject: [PATCH 32/40] Chore: improved perfomance of ime operations --- .../raw_editor/input/ime/on_delete.dart | 4 +- .../raw_editor/input/ime/on_insert.dart | 9 ++- .../input/ime/on_non_update_text.dart | 4 +- .../input/ime/on_replace_method.dart | 78 ++++++------------- .../raw_editor_state_input_client_mixin.dart | 12 +-- 5 files changed, 40 insertions(+), 67 deletions(-) diff --git a/lib/src/editor/raw_editor/input/ime/on_delete.dart b/lib/src/editor/raw_editor/input/ime/on_delete.dart index 2632e633c..f56e53c5f 100644 --- a/lib/src/editor/raw_editor/input/ime/on_delete.dart +++ b/lib/src/editor/raw_editor/input/ime/on_delete.dart @@ -1,10 +1,10 @@ import 'package:flutter/services.dart'; import '../../../../../flutter_quill.dart'; -Future onDelete( +void onDelete( TextEditingDeltaDeletion deletion, QuillController controller, -) async { +) { final start = deletion.deletedRange.start; final length = deletion.deletedRange.end - start; controller.replaceText( diff --git a/lib/src/editor/raw_editor/input/ime/on_insert.dart b/lib/src/editor/raw_editor/input/ime/on_insert.dart index 43f3cb538..61c5b1f26 100644 --- a/lib/src/editor/raw_editor/input/ime/on_insert.dart +++ b/lib/src/editor/raw_editor/input/ime/on_insert.dart @@ -3,18 +3,19 @@ import 'package:flutter/services.dart'; import '../../../../controller/quill_controller.dart'; import '../../../raw_editor/config/events/character_shortcuts_events.dart'; -Future onInsert( +void onInsert( TextEditingDeltaInsertion insertion, QuillController controller, List characterShortcutEvents, -) async { - final selection = insertion.selection; +) { + final selection = controller.selection; final insertionText = insertion.textInserted; if (insertionText.length == 1 && !insertionText.contains('\n')) { for (final shortcutEvent in characterShortcutEvents) { - if (shortcutEvent.character == insertionText && shortcutEvent.handler(controller)) { + if (shortcutEvent.character == insertionText && + shortcutEvent.handler(controller)) { return; } } diff --git a/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart b/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart index d5395f8b8..0eea0c5c4 100644 --- a/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart +++ b/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart @@ -2,10 +2,10 @@ import 'package:flutter/services.dart'; import '../../../../../flutter_quill.dart'; import '../../../../../internal.dart'; -Future onNonTextUpdate( +void onNonTextUpdate( TextEditingDeltaNonTextUpdate nonTextUpdate, QuillController controller, -) async { +) { final effectiveSelection = TextSelection.collapsed(offset: nonTextUpdate.selection.baseOffset); // when typing characters with CJK IME on Windows, a non-text update is sent diff --git a/lib/src/editor/raw_editor/input/ime/on_replace_method.dart b/lib/src/editor/raw_editor/input/ime/on_replace_method.dart index ee7508a73..395aca17a 100644 --- a/lib/src/editor/raw_editor/input/ime/on_replace_method.dart +++ b/lib/src/editor/raw_editor/input/ime/on_replace_method.dart @@ -2,69 +2,39 @@ import 'package:flutter/services.dart'; import '../../../../../internal.dart'; import '../../../../controller/quill_controller.dart'; import '../../../raw_editor/config/events/character_shortcuts_events.dart'; -import 'on_insert.dart'; -Future onReplace( +void onReplace( TextEditingDeltaReplacement replacement, QuillController controller, List characterShortcutEvents, -) async { +) { // delete the selection - final selection = replacement.selection; + final selection = controller.selection; final textReplacement = replacement.replacementText; - if (selection.isCollapsed) { - if (isIosApp) { - // remove the trailing '\n' when pressing the return key - if (textReplacement.endsWith('\n')) { - replacement = TextEditingDeltaReplacement( - oldText: replacement.oldText, - replacementText: replacement.replacementText.substring( - 0, - replacement.replacementText.length - 1, - ), - replacedRange: replacement.replacedRange, - selection: replacement.selection, - composing: replacement.composing, - ); - } - } - - final insertion = replacement.toInsertion(); - await onInsert( - insertion, - controller, - characterShortcutEvents, - ); - } else { - final start = replacement.replacedRange.start; - final length = replacement.replacedRange.end - start; - controller.replaceText( - start, - length, - replacement.replacementText, - TextSelection.collapsed( - offset: selection.baseOffset, - affinity: selection.affinity, + if (selection.isCollapsed && isIosApp && textReplacement.endsWith('\n')) { + // remove the trailing '\n' when pressing the return key + replacement = TextEditingDeltaReplacement( + oldText: replacement.oldText, + replacementText: replacement.replacementText.substring( + 0, + replacement.replacementText.length - 1, ), + replacedRange: replacement.replacedRange, + selection: replacement.selection, + composing: replacement.composing, ); } -} - -extension on TextEditingDeltaReplacement { - TextEditingDeltaInsertion toInsertion() { - final text = oldText.replaceRange( - replacedRange.start, - replacedRange.end, - '', - ); - return TextEditingDeltaInsertion( - oldText: text, - textInserted: replacementText, - insertionOffset: replacedRange.start, - selection: selection, - composing: composing, - ); - } + final start = replacement.replacedRange.start; + final length = replacement.replacedRange.end - start; + controller.replaceText( + start, + length, + replacement.replacementText, + TextSelection.collapsed( + offset: selection.baseOffset, + affinity: selection.affinity, + ), + ); } diff --git a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart index 074abefa5..62ea0ca64 100644 --- a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart @@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart' show ValueNotifier, kIsWeb; import 'package:flutter/material.dart' show Theme; import 'package:flutter/scheduler.dart' show SchedulerBinding; import 'package:flutter/services.dart'; +import '../../../delta/delta_diff.dart'; +import '../../../document/document.dart'; import '../raw_editor.dart'; import 'diff_services.dart'; import 'ime/on_delete.dart'; @@ -227,27 +229,27 @@ mixin RawEditorStateTextInputClientMixin on EditorState _apply([textEditingDlta]); } - Future _apply(List deltas) async { + void _apply(List deltas) { for (final delta in deltas) { if (delta is TextEditingDeltaInsertion) { - await onInsert( + onInsert( delta, widget.controller, widget.config.characterShortcutEvents, ); } else if (delta is TextEditingDeltaDeletion) { - await onDelete( + onDelete( delta, widget.controller, ); } else if (delta is TextEditingDeltaReplacement) { - await onReplace( + onReplace( delta, widget.controller, widget.config.characterShortcutEvents, ); } else if (delta is TextEditingDeltaNonTextUpdate) { - await onNonTextUpdate( + onNonTextUpdate( delta, widget.controller, ); From 155491f2bbe712c16299bb8f1f42be36f4c41c15 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Thu, 13 Mar 2025 01:33:41 -0400 Subject: [PATCH 33/40] Chore: removed unused imports in text_input_mixin --- lib/src/editor/raw_editor/input/ime/on_non_update_text.dart | 3 +-- .../raw_editor/input/raw_editor_state_input_client_mixin.dart | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart b/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart index 0eea0c5c4..fe39dc137 100644 --- a/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart +++ b/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart @@ -6,8 +6,7 @@ void onNonTextUpdate( TextEditingDeltaNonTextUpdate nonTextUpdate, QuillController controller, ) { - final effectiveSelection = - TextSelection.collapsed(offset: nonTextUpdate.selection.baseOffset); + final effectiveSelection = nonTextUpdate.selection; // when typing characters with CJK IME on Windows, a non-text update is sent // with the selection range. if (isWindowsApp) { diff --git a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart index 62ea0ca64..ac170f52d 100644 --- a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart @@ -6,8 +6,6 @@ import 'package:flutter/foundation.dart' show ValueNotifier, kIsWeb; import 'package:flutter/material.dart' show Theme; import 'package:flutter/scheduler.dart' show SchedulerBinding; import 'package:flutter/services.dart'; -import '../../../delta/delta_diff.dart'; -import '../../../document/document.dart'; import '../raw_editor.dart'; import 'diff_services.dart'; import 'ime/on_delete.dart'; From 1d0aa63fefff0f70de57818c5e01eb96f15f0fe2 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Fri, 28 Mar 2025 20:41:13 -0400 Subject: [PATCH 34/40] Chore: implemented delta_text_input_client instead diffing string changes --- .../raw_editor/input/diff_services.dart | 56 -------------- .../raw_editor_state_input_client_mixin.dart | 73 ++++++++----------- 2 files changed, 31 insertions(+), 98 deletions(-) delete mode 100644 lib/src/editor/raw_editor/input/diff_services.dart diff --git a/lib/src/editor/raw_editor/input/diff_services.dart b/lib/src/editor/raw_editor/input/diff_services.dart deleted file mode 100644 index efea823a5..000000000 --- a/lib/src/editor/raw_editor/input/diff_services.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/services.dart'; -import '../../../delta/delta_diff.dart'; - -/// Return a list of the change type that was do it to the content of the editor -TextEditingDelta getTextEditingDelta( - TextEditingValue oldValue, - TextEditingValue newValue, -) { - // we need to check why sometimes in android, when we place the caret - // at a position, it moves backward unexpectly. By now, i think that we need to use - // the removed Debounce class to wait for the android soft-keyboard events - // since on android, non-text-update is called more times that we think - final currentText = oldValue.text; - final diff = getDiff( - currentText, - newValue.text, - newValue.selection.extentOffset, - ); - if (diff.inserted.isEmpty && diff.deleted.isEmpty) { - return TextEditingDeltaNonTextUpdate( - oldText: newValue.text, - selection: newValue.selection, - composing: newValue.composing, - ); - } else if (diff.inserted.isNotEmpty && diff.deleted.isEmpty) { - return TextEditingDeltaInsertion( - oldText: currentText, - textInserted: diff.inserted, - insertionOffset: diff.start, - selection: newValue.selection, - composing: newValue.composing, - ); - } else if (diff.inserted.isEmpty && diff.deleted.isNotEmpty) { - return TextEditingDeltaDeletion( - oldText: currentText, - selection: newValue.selection, - composing: newValue.composing, - deletedRange: TextRange( - start: diff.start, - end: diff.start + diff.deleted.length, - ), - ); - } else if (diff.inserted.isNotEmpty && diff.deleted.isNotEmpty) { - return TextEditingDeltaReplacement( - oldText: currentText, - selection: newValue.selection, - composing: newValue.composing, - replacementText: diff.inserted, - replacedRange: TextRange( - start: diff.start, - end: diff.start + diff.deleted.length, - ), - ); - } - throw UnsupportedError('Unknown diff: $diff'); -} diff --git a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart index ac170f52d..b069ad73f 100644 --- a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart @@ -7,14 +7,13 @@ import 'package:flutter/material.dart' show Theme; import 'package:flutter/scheduler.dart' show SchedulerBinding; import 'package:flutter/services.dart'; import '../raw_editor.dart'; -import 'diff_services.dart'; import 'ime/on_delete.dart'; import 'ime/on_insert.dart'; import 'ime/on_non_update_text.dart'; import 'ime/on_replace_method.dart'; mixin RawEditorStateTextInputClientMixin on EditorState - implements TextInputClient { + implements TextInputClient, DeltaTextInputClient { TextInputConnection? _textInputConnection; TextEditingValue? __lastKnownRemoteTextEditingValue; @@ -75,6 +74,8 @@ mixin RawEditorStateTextInputClientMixin on EditorState TextInputConfiguration( inputType: TextInputType.multiline, readOnly: widget.config.readOnly, + enableDeltaModel: true, + enableIMEPersonalizedLearning: true, inputAction: widget.config.textInputAction, enableSuggestions: !widget.config.readOnly, keyboardAppearance: widget.config.keyboardAppearance ?? @@ -97,12 +98,9 @@ mixin RawEditorStateTextInputClientMixin on EditorState if (_lastKnownRemoteTextEditingValue != null) { if (_lastKnownRemoteTextEditingValue!.selection.end > _lastKnownRemoteTextEditingValue!.text.length) { - _lastKnownRemoteTextEditingValue = _lastKnownRemoteTextEditingValue! - .copyWith( - selection: _lastKnownRemoteTextEditingValue!.selection - .copyWith( - extentOffset: - _lastKnownRemoteTextEditingValue!.text.length)); + _lastKnownRemoteTextEditingValue = _lastKnownRemoteTextEditingValue!.copyWith( + selection: _lastKnownRemoteTextEditingValue!.selection + .copyWith(extentOffset: _lastKnownRemoteTextEditingValue!.text.length)); } } _textInputConnection!.setEditingState(_lastKnownRemoteTextEditingValue!); @@ -112,8 +110,8 @@ mixin RawEditorStateTextInputClientMixin on EditorState // windows void _updateComposingRectIfNeeded() { - final composingRange = _lastKnownRemoteTextEditingValue?.composing ?? - textEditingValue.composing; + final composingRange = + _lastKnownRemoteTextEditingValue?.composing ?? textEditingValue.composing; if (hasConnection) { assert(mounted); if (composingRange.isValid) { @@ -135,12 +133,10 @@ mixin RawEditorStateTextInputClientMixin on EditorState renderEditor.selection.isCollapsed) { final currentTextPosition = TextPosition(offset: renderEditor.selection.baseOffset); - final caretRect = - renderEditor.getLocalRectForCaret(currentTextPosition); + final caretRect = renderEditor.getLocalRectForCaret(currentTextPosition); _textInputConnection!.setCaretRect(caretRect); } - SchedulerBinding.instance - .addPostFrameCallback((_) => _updateCaretRectIfNeeded()); + SchedulerBinding.instance.addPostFrameCallback((_) => _updateCaretRectIfNeeded()); } } @@ -191,8 +187,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState // Start TextInputClient implementation @override - TextEditingValue? get currentTextEditingValue => - _lastKnownRemoteTextEditingValue; + TextEditingValue? get currentTextEditingValue => _lastKnownRemoteTextEditingValue; // autofill is not needed @override @@ -203,8 +198,8 @@ mixin RawEditorStateTextInputClientMixin on EditorState if (!shouldCreateInputConnection) { return; } - if (_lastKnownRemoteTextEditingValue == value) { + // There is no difference between this value and the last known value. return; } @@ -221,10 +216,12 @@ mixin RawEditorStateTextInputClientMixin on EditorState return; } - final textEditingDlta = - getTextEditingDelta(_lastKnownRemoteTextEditingValue!, value); _lastKnownRemoteTextEditingValue = value; - _apply([textEditingDlta]); + } + + @override + void updateEditingValueWithDeltas(List textEditingDeltas) { + _apply(textEditingDeltas); } void _apply(List deltas) { @@ -302,11 +299,10 @@ mixin RawEditorStateTextInputClientMixin on EditorState final currentTextPosition = TextPosition(offset: renderEditor.selection.baseOffset); - _startCaretRect = - renderEditor.getLocalRectForCaret(currentTextPosition); + _startCaretRect = renderEditor.getLocalRectForCaret(currentTextPosition); - _lastBoundedOffset = _startCaretRect!.center - - _floatingCursorOffset(currentTextPosition); + _lastBoundedOffset = + _startCaretRect!.center - _floatingCursorOffset(currentTextPosition); _lastTextPosition = currentTextPosition; renderEditor.setFloatingCursor( point.state, _lastBoundedOffset!, _lastTextPosition!); @@ -318,31 +314,27 @@ mixin RawEditorStateTextInputClientMixin on EditorState final rawCursorOffset = _startCaretRect!.center + centeredPoint - floatingCursorOffset; - final preferredLineHeight = - renderEditor.preferredLineHeight(_lastTextPosition!); + final preferredLineHeight = renderEditor.preferredLineHeight(_lastTextPosition!); _lastBoundedOffset = renderEditor.calculateBoundedFloatingCursorOffset( rawCursorOffset, preferredLineHeight, ); - _lastTextPosition = renderEditor.getPositionForOffset(renderEditor - .localToGlobal(_lastBoundedOffset! + floatingCursorOffset)); + _lastTextPosition = renderEditor.getPositionForOffset( + renderEditor.localToGlobal(_lastBoundedOffset! + floatingCursorOffset)); renderEditor.setFloatingCursor( point.state, _lastBoundedOffset!, _lastTextPosition!); final newSelection = TextSelection.collapsed( - offset: _lastTextPosition!.offset, - affinity: _lastTextPosition!.affinity); + offset: _lastTextPosition!.offset, affinity: _lastTextPosition!.affinity); // Setting selection as floating cursor moves will have scroll view // bring background cursor into view - renderEditor.onSelectionChanged( - newSelection, SelectionChangedCause.forcePress); + renderEditor.onSelectionChanged(newSelection, SelectionChangedCause.forcePress); break; case FloatingCursorDragState.End: // We skip animation if no update has happened. if (_lastTextPosition != null && _lastBoundedOffset != null) { floatingCursorResetController ..value = 0.0 - ..animateTo(1, - duration: _floatingCursorResetTime, curve: Curves.decelerate); + ..animateTo(1, duration: _floatingCursorResetTime, curve: Curves.decelerate); } break; } @@ -367,13 +359,11 @@ mixin RawEditorStateTextInputClientMixin on EditorState _lastBoundedOffset = null; } else { final lerpValue = floatingCursorResetController.value; - final lerpX = - lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!; - final lerpY = - lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!; + final lerpX = lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!; + final lerpY = lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!; - renderEditor.setFloatingCursor(FloatingCursorDragState.Update, - Offset(lerpX, lerpY), _lastTextPosition!, + renderEditor.setFloatingCursor( + FloatingCursorDragState.Update, Offset(lerpX, lerpY), _lastTextPosition!, resetLerpValue: lerpValue); } } @@ -401,8 +391,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState final size = renderEditor.size; final transform = renderEditor.getTransformTo(null); _textInputConnection?.setEditableSizeAndTransform(size, transform); - SchedulerBinding.instance - .addPostFrameCallback((_) => _updateSizeAndTransform()); + SchedulerBinding.instance.addPostFrameCallback((_) => _updateSizeAndTransform()); } } } From 2936b93090d6585d45d2a51da418cb7211765db6 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Fri, 28 Mar 2025 20:42:29 -0400 Subject: [PATCH 35/40] Chore: format --- .../raw_editor_state_input_client_mixin.dart | 61 +++++++++++-------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart index b069ad73f..fa846141e 100644 --- a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart @@ -75,7 +75,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState inputType: TextInputType.multiline, readOnly: widget.config.readOnly, enableDeltaModel: true, - enableIMEPersonalizedLearning: true, + enableIMEPersonalizedLearning: true, inputAction: widget.config.textInputAction, enableSuggestions: !widget.config.readOnly, keyboardAppearance: widget.config.keyboardAppearance ?? @@ -98,9 +98,12 @@ mixin RawEditorStateTextInputClientMixin on EditorState if (_lastKnownRemoteTextEditingValue != null) { if (_lastKnownRemoteTextEditingValue!.selection.end > _lastKnownRemoteTextEditingValue!.text.length) { - _lastKnownRemoteTextEditingValue = _lastKnownRemoteTextEditingValue!.copyWith( - selection: _lastKnownRemoteTextEditingValue!.selection - .copyWith(extentOffset: _lastKnownRemoteTextEditingValue!.text.length)); + _lastKnownRemoteTextEditingValue = _lastKnownRemoteTextEditingValue! + .copyWith( + selection: _lastKnownRemoteTextEditingValue!.selection + .copyWith( + extentOffset: + _lastKnownRemoteTextEditingValue!.text.length)); } } _textInputConnection!.setEditingState(_lastKnownRemoteTextEditingValue!); @@ -110,8 +113,8 @@ mixin RawEditorStateTextInputClientMixin on EditorState // windows void _updateComposingRectIfNeeded() { - final composingRange = - _lastKnownRemoteTextEditingValue?.composing ?? textEditingValue.composing; + final composingRange = _lastKnownRemoteTextEditingValue?.composing ?? + textEditingValue.composing; if (hasConnection) { assert(mounted); if (composingRange.isValid) { @@ -133,10 +136,12 @@ mixin RawEditorStateTextInputClientMixin on EditorState renderEditor.selection.isCollapsed) { final currentTextPosition = TextPosition(offset: renderEditor.selection.baseOffset); - final caretRect = renderEditor.getLocalRectForCaret(currentTextPosition); + final caretRect = + renderEditor.getLocalRectForCaret(currentTextPosition); _textInputConnection!.setCaretRect(caretRect); } - SchedulerBinding.instance.addPostFrameCallback((_) => _updateCaretRectIfNeeded()); + SchedulerBinding.instance + .addPostFrameCallback((_) => _updateCaretRectIfNeeded()); } } @@ -187,7 +192,8 @@ mixin RawEditorStateTextInputClientMixin on EditorState // Start TextInputClient implementation @override - TextEditingValue? get currentTextEditingValue => _lastKnownRemoteTextEditingValue; + TextEditingValue? get currentTextEditingValue => + _lastKnownRemoteTextEditingValue; // autofill is not needed @override @@ -199,7 +205,6 @@ mixin RawEditorStateTextInputClientMixin on EditorState return; } if (_lastKnownRemoteTextEditingValue == value) { - // There is no difference between this value and the last known value. return; } @@ -299,10 +304,11 @@ mixin RawEditorStateTextInputClientMixin on EditorState final currentTextPosition = TextPosition(offset: renderEditor.selection.baseOffset); - _startCaretRect = renderEditor.getLocalRectForCaret(currentTextPosition); + _startCaretRect = + renderEditor.getLocalRectForCaret(currentTextPosition); - _lastBoundedOffset = - _startCaretRect!.center - _floatingCursorOffset(currentTextPosition); + _lastBoundedOffset = _startCaretRect!.center - + _floatingCursorOffset(currentTextPosition); _lastTextPosition = currentTextPosition; renderEditor.setFloatingCursor( point.state, _lastBoundedOffset!, _lastTextPosition!); @@ -314,27 +320,31 @@ mixin RawEditorStateTextInputClientMixin on EditorState final rawCursorOffset = _startCaretRect!.center + centeredPoint - floatingCursorOffset; - final preferredLineHeight = renderEditor.preferredLineHeight(_lastTextPosition!); + final preferredLineHeight = + renderEditor.preferredLineHeight(_lastTextPosition!); _lastBoundedOffset = renderEditor.calculateBoundedFloatingCursorOffset( rawCursorOffset, preferredLineHeight, ); - _lastTextPosition = renderEditor.getPositionForOffset( - renderEditor.localToGlobal(_lastBoundedOffset! + floatingCursorOffset)); + _lastTextPosition = renderEditor.getPositionForOffset(renderEditor + .localToGlobal(_lastBoundedOffset! + floatingCursorOffset)); renderEditor.setFloatingCursor( point.state, _lastBoundedOffset!, _lastTextPosition!); final newSelection = TextSelection.collapsed( - offset: _lastTextPosition!.offset, affinity: _lastTextPosition!.affinity); + offset: _lastTextPosition!.offset, + affinity: _lastTextPosition!.affinity); // Setting selection as floating cursor moves will have scroll view // bring background cursor into view - renderEditor.onSelectionChanged(newSelection, SelectionChangedCause.forcePress); + renderEditor.onSelectionChanged( + newSelection, SelectionChangedCause.forcePress); break; case FloatingCursorDragState.End: // We skip animation if no update has happened. if (_lastTextPosition != null && _lastBoundedOffset != null) { floatingCursorResetController ..value = 0.0 - ..animateTo(1, duration: _floatingCursorResetTime, curve: Curves.decelerate); + ..animateTo(1, + duration: _floatingCursorResetTime, curve: Curves.decelerate); } break; } @@ -359,11 +369,13 @@ mixin RawEditorStateTextInputClientMixin on EditorState _lastBoundedOffset = null; } else { final lerpValue = floatingCursorResetController.value; - final lerpX = lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!; - final lerpY = lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!; + final lerpX = + lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!; + final lerpY = + lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!; - renderEditor.setFloatingCursor( - FloatingCursorDragState.Update, Offset(lerpX, lerpY), _lastTextPosition!, + renderEditor.setFloatingCursor(FloatingCursorDragState.Update, + Offset(lerpX, lerpY), _lastTextPosition!, resetLerpValue: lerpValue); } } @@ -391,7 +403,8 @@ mixin RawEditorStateTextInputClientMixin on EditorState final size = renderEditor.size; final transform = renderEditor.getTransformTo(null); _textInputConnection?.setEditableSizeAndTransform(size, transform); - SchedulerBinding.instance.addPostFrameCallback((_) => _updateSizeAndTransform()); + SchedulerBinding.instance + .addPostFrameCallback((_) => _updateSizeAndTransform()); } } } From a9f2b5b29de276bc98c3f6446e1330c2476b217f Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Fri, 28 Mar 2025 20:59:36 -0400 Subject: [PATCH 36/40] Chore: removed TextInputClient by recommendation of flutter docs --- .../raw_editor/input/raw_editor_state_input_client_mixin.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart index fa846141e..b6ee8f9c8 100644 --- a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart @@ -13,7 +13,7 @@ import 'ime/on_non_update_text.dart'; import 'ime/on_replace_method.dart'; mixin RawEditorStateTextInputClientMixin on EditorState - implements TextInputClient, DeltaTextInputClient { + implements DeltaTextInputClient { TextInputConnection? _textInputConnection; TextEditingValue? __lastKnownRemoteTextEditingValue; From 44bdd3d4c247e444d807513bbc31938c35b3c489 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Fri, 28 Mar 2025 21:11:03 -0400 Subject: [PATCH 37/40] Chore: removed use of updateEditingValue by recommendation of flutter docs --- .../raw_editor_state_input_client_mixin.dart | 99 +++++++------------ 1 file changed, 38 insertions(+), 61 deletions(-) diff --git a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart index b6ee8f9c8..1c10c655f 100644 --- a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart @@ -12,8 +12,7 @@ import 'ime/on_insert.dart'; import 'ime/on_non_update_text.dart'; import 'ime/on_replace_method.dart'; -mixin RawEditorStateTextInputClientMixin on EditorState - implements DeltaTextInputClient { +mixin RawEditorStateTextInputClientMixin on EditorState implements DeltaTextInputClient { TextInputConnection? _textInputConnection; TextEditingValue? __lastKnownRemoteTextEditingValue; @@ -98,12 +97,9 @@ mixin RawEditorStateTextInputClientMixin on EditorState if (_lastKnownRemoteTextEditingValue != null) { if (_lastKnownRemoteTextEditingValue!.selection.end > _lastKnownRemoteTextEditingValue!.text.length) { - _lastKnownRemoteTextEditingValue = _lastKnownRemoteTextEditingValue! - .copyWith( - selection: _lastKnownRemoteTextEditingValue!.selection - .copyWith( - extentOffset: - _lastKnownRemoteTextEditingValue!.text.length)); + _lastKnownRemoteTextEditingValue = _lastKnownRemoteTextEditingValue!.copyWith( + selection: _lastKnownRemoteTextEditingValue!.selection + .copyWith(extentOffset: _lastKnownRemoteTextEditingValue!.text.length)); } } _textInputConnection!.setEditingState(_lastKnownRemoteTextEditingValue!); @@ -113,8 +109,8 @@ mixin RawEditorStateTextInputClientMixin on EditorState // windows void _updateComposingRectIfNeeded() { - final composingRange = _lastKnownRemoteTextEditingValue?.composing ?? - textEditingValue.composing; + final composingRange = + _lastKnownRemoteTextEditingValue?.composing ?? textEditingValue.composing; if (hasConnection) { assert(mounted); if (composingRange.isValid) { @@ -136,12 +132,10 @@ mixin RawEditorStateTextInputClientMixin on EditorState renderEditor.selection.isCollapsed) { final currentTextPosition = TextPosition(offset: renderEditor.selection.baseOffset); - final caretRect = - renderEditor.getLocalRectForCaret(currentTextPosition); + final caretRect = renderEditor.getLocalRectForCaret(currentTextPosition); _textInputConnection!.setCaretRect(caretRect); } - SchedulerBinding.instance - .addPostFrameCallback((_) => _updateCaretRectIfNeeded()); + SchedulerBinding.instance.addPostFrameCallback((_) => _updateCaretRectIfNeeded()); } } @@ -192,45 +186,36 @@ mixin RawEditorStateTextInputClientMixin on EditorState // Start TextInputClient implementation @override - TextEditingValue? get currentTextEditingValue => - _lastKnownRemoteTextEditingValue; + TextEditingValue? get currentTextEditingValue => _lastKnownRemoteTextEditingValue; // autofill is not needed @override AutofillScope? get currentAutofillScope => null; @override - void updateEditingValue(TextEditingValue value) { - if (!shouldCreateInputConnection) { - return; - } - if (_lastKnownRemoteTextEditingValue == value) { - // There is no difference between this value and the last known value. - return; - } - - // Check if only composing range changed. - if (_lastKnownRemoteTextEditingValue!.text == value.text && - _lastKnownRemoteTextEditingValue!.selection == value.selection) { - // This update only modifies composing range. Since we don't keep track - // of composing range we just need to update last known value here. - // This check fixes an issue on Android when it sends - // composing updates separately from regular changes for text and - // selection. - _lastKnownRemoteTextEditingValue = value; - return; - } - - _lastKnownRemoteTextEditingValue = value; - } + void updateEditingValue(TextEditingValue value) {} @override void updateEditingValueWithDeltas(List textEditingDeltas) { + if (!shouldCreateInputConnection) { + return; + } _apply(textEditingDeltas); } void _apply(List deltas) { for (final delta in deltas) { + // updates _lastKnownRemoteTextEditingValue to avoid issues + + _lastKnownRemoteTextEditingValue = delta.apply( + TextEditingValue( + text: _lastKnownRemoteTextEditingValue?.text ?? + widget.controller.document.toPlainText(), + selection: + _lastKnownRemoteTextEditingValue?.selection ?? widget.controller.selection, + composing: _lastKnownRemoteTextEditingValue?.composing ?? TextRange.empty, + ), + ); if (delta is TextEditingDeltaInsertion) { onInsert( delta, @@ -304,11 +289,10 @@ mixin RawEditorStateTextInputClientMixin on EditorState final currentTextPosition = TextPosition(offset: renderEditor.selection.baseOffset); - _startCaretRect = - renderEditor.getLocalRectForCaret(currentTextPosition); + _startCaretRect = renderEditor.getLocalRectForCaret(currentTextPosition); - _lastBoundedOffset = _startCaretRect!.center - - _floatingCursorOffset(currentTextPosition); + _lastBoundedOffset = + _startCaretRect!.center - _floatingCursorOffset(currentTextPosition); _lastTextPosition = currentTextPosition; renderEditor.setFloatingCursor( point.state, _lastBoundedOffset!, _lastTextPosition!); @@ -320,31 +304,27 @@ mixin RawEditorStateTextInputClientMixin on EditorState final rawCursorOffset = _startCaretRect!.center + centeredPoint - floatingCursorOffset; - final preferredLineHeight = - renderEditor.preferredLineHeight(_lastTextPosition!); + final preferredLineHeight = renderEditor.preferredLineHeight(_lastTextPosition!); _lastBoundedOffset = renderEditor.calculateBoundedFloatingCursorOffset( rawCursorOffset, preferredLineHeight, ); - _lastTextPosition = renderEditor.getPositionForOffset(renderEditor - .localToGlobal(_lastBoundedOffset! + floatingCursorOffset)); + _lastTextPosition = renderEditor.getPositionForOffset( + renderEditor.localToGlobal(_lastBoundedOffset! + floatingCursorOffset)); renderEditor.setFloatingCursor( point.state, _lastBoundedOffset!, _lastTextPosition!); final newSelection = TextSelection.collapsed( - offset: _lastTextPosition!.offset, - affinity: _lastTextPosition!.affinity); + offset: _lastTextPosition!.offset, affinity: _lastTextPosition!.affinity); // Setting selection as floating cursor moves will have scroll view // bring background cursor into view - renderEditor.onSelectionChanged( - newSelection, SelectionChangedCause.forcePress); + renderEditor.onSelectionChanged(newSelection, SelectionChangedCause.forcePress); break; case FloatingCursorDragState.End: // We skip animation if no update has happened. if (_lastTextPosition != null && _lastBoundedOffset != null) { floatingCursorResetController ..value = 0.0 - ..animateTo(1, - duration: _floatingCursorResetTime, curve: Curves.decelerate); + ..animateTo(1, duration: _floatingCursorResetTime, curve: Curves.decelerate); } break; } @@ -369,13 +349,11 @@ mixin RawEditorStateTextInputClientMixin on EditorState _lastBoundedOffset = null; } else { final lerpValue = floatingCursorResetController.value; - final lerpX = - lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!; - final lerpY = - lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!; + final lerpX = lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!; + final lerpY = lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!; - renderEditor.setFloatingCursor(FloatingCursorDragState.Update, - Offset(lerpX, lerpY), _lastTextPosition!, + renderEditor.setFloatingCursor( + FloatingCursorDragState.Update, Offset(lerpX, lerpY), _lastTextPosition!, resetLerpValue: lerpValue); } } @@ -403,8 +381,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState final size = renderEditor.size; final transform = renderEditor.getTransformTo(null); _textInputConnection?.setEditableSizeAndTransform(size, transform); - SchedulerBinding.instance - .addPostFrameCallback((_) => _updateSizeAndTransform()); + SchedulerBinding.instance.addPostFrameCallback((_) => _updateSizeAndTransform()); } } } From 924a27ba64b717e2e0b02ff90e734335045cfa45 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Fri, 28 Mar 2025 21:25:31 -0400 Subject: [PATCH 38/40] Chore: added method for apply deltas to the last known editing value --- .../raw_editor_state_input_client_mixin.dart | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart index 8836adb20..9801c44be 100644 --- a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart @@ -203,7 +203,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements DeltaTextInpu @override void updateEditingValueWithDeltas(List textEditingDeltas) { - if (!shouldCreateInputConnection) { + if (!shouldCreateInputConnection || textEditingDeltas.isEmpty) { return; } _apply(textEditingDeltas); @@ -212,16 +212,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements DeltaTextInpu void _apply(List deltas) { for (final delta in deltas) { // updates _lastKnownRemoteTextEditingValue to avoid issues - - _lastKnownRemoteTextEditingValue = delta.apply( - TextEditingValue( - text: _lastKnownRemoteTextEditingValue?.text ?? - widget.controller.document.toPlainText(), - selection: - _lastKnownRemoteTextEditingValue?.selection ?? widget.controller.selection, - composing: _lastKnownRemoteTextEditingValue?.composing ?? TextRange.empty, - ), - ); + _updateLastKnownRemoteTextEditingValueWithDeltas(delta); if (delta is TextEditingDeltaInsertion) { onInsert( delta, @@ -380,6 +371,14 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements DeltaTextInpu _lastKnownRemoteTextEditingValue = null; } + @visibleForTesting + @internal + void updateLastKnownRemoteTextEditingValueWithDeltas(TextEditingDelta delta) { + // Apply the deltas to the previous platform-side IME value, to find out + // what the platform thinks the IME value is + _lastKnownRemoteTextEditingValue = delta.apply(_lastKnownRemoteTextEditingValue!); + } + void _updateSizeAndTransform() { if (hasConnection) { // Asking for renderEditor.size here can cause errors if layout hasn't From 74287ae47c31798a8496b73101ea651c21603d12 Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Fri, 28 Mar 2025 21:27:17 -0400 Subject: [PATCH 39/40] Fix: missed method name change --- .../raw_editor/input/raw_editor_state_input_client_mixin.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart index 9801c44be..4dbbe6515 100644 --- a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart @@ -212,7 +212,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements DeltaTextInpu void _apply(List deltas) { for (final delta in deltas) { // updates _lastKnownRemoteTextEditingValue to avoid issues - _updateLastKnownRemoteTextEditingValueWithDeltas(delta); + updateLastKnownRemoteTextEditingValueWithDeltas(delta); if (delta is TextEditingDeltaInsertion) { onInsert( delta, From dc438bf3ee7bc44d80fd688e344497bc83763d9a Mon Sep 17 00:00:00 2001 From: CatHood0 Date: Fri, 28 Mar 2025 21:34:35 -0400 Subject: [PATCH 40/40] Chore: moved ime helpers to be part of ime_internals file --- .../raw_editor/input/ime/ime_internals.dart | 13 ++++ .../raw_editor/input/ime/on_delete.dart | 3 +- .../raw_editor/input/ime/on_insert.dart | 5 +- .../input/ime/on_non_update_text.dart | 4 +- .../input/ime/on_replace_method.dart | 5 +- .../raw_editor_state_input_client_mixin.dart | 69 +++++++++++-------- 6 files changed, 58 insertions(+), 41 deletions(-) create mode 100644 lib/src/editor/raw_editor/input/ime/ime_internals.dart diff --git a/lib/src/editor/raw_editor/input/ime/ime_internals.dart b/lib/src/editor/raw_editor/input/ime/ime_internals.dart new file mode 100644 index 000000000..46b842602 --- /dev/null +++ b/lib/src/editor/raw_editor/input/ime/ime_internals.dart @@ -0,0 +1,13 @@ +@internal +library; + +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; + +import '../../../../../flutter_quill.dart'; +import '../../../../../internal.dart'; + +part 'on_insert.dart'; +part 'on_delete.dart'; +part 'on_replace_method.dart'; +part 'on_non_update_text.dart'; diff --git a/lib/src/editor/raw_editor/input/ime/on_delete.dart b/lib/src/editor/raw_editor/input/ime/on_delete.dart index f56e53c5f..75bf196fb 100644 --- a/lib/src/editor/raw_editor/input/ime/on_delete.dart +++ b/lib/src/editor/raw_editor/input/ime/on_delete.dart @@ -1,5 +1,4 @@ -import 'package:flutter/services.dart'; -import '../../../../../flutter_quill.dart'; +part of 'ime_internals.dart'; void onDelete( TextEditingDeltaDeletion deletion, diff --git a/lib/src/editor/raw_editor/input/ime/on_insert.dart b/lib/src/editor/raw_editor/input/ime/on_insert.dart index 61c5b1f26..49504a8bc 100644 --- a/lib/src/editor/raw_editor/input/ime/on_insert.dart +++ b/lib/src/editor/raw_editor/input/ime/on_insert.dart @@ -1,7 +1,4 @@ -import 'package:flutter/services.dart'; - -import '../../../../controller/quill_controller.dart'; -import '../../../raw_editor/config/events/character_shortcuts_events.dart'; +part of 'ime_internals.dart'; void onInsert( TextEditingDeltaInsertion insertion, diff --git a/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart b/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart index fe39dc137..0956047ee 100644 --- a/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart +++ b/lib/src/editor/raw_editor/input/ime/on_non_update_text.dart @@ -1,6 +1,4 @@ -import 'package:flutter/services.dart'; -import '../../../../../flutter_quill.dart'; -import '../../../../../internal.dart'; +part of 'ime_internals.dart'; void onNonTextUpdate( TextEditingDeltaNonTextUpdate nonTextUpdate, diff --git a/lib/src/editor/raw_editor/input/ime/on_replace_method.dart b/lib/src/editor/raw_editor/input/ime/on_replace_method.dart index 395aca17a..b46c8ae18 100644 --- a/lib/src/editor/raw_editor/input/ime/on_replace_method.dart +++ b/lib/src/editor/raw_editor/input/ime/on_replace_method.dart @@ -1,7 +1,4 @@ -import 'package:flutter/services.dart'; -import '../../../../../internal.dart'; -import '../../../../controller/quill_controller.dart'; -import '../../../raw_editor/config/events/character_shortcuts_events.dart'; +part of 'ime_internals.dart'; void onReplace( TextEditingDeltaReplacement replacement, diff --git a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart index 4dbbe6515..56aff41ee 100644 --- a/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/input/raw_editor_state_input_client_mixin.dart @@ -7,12 +7,10 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:meta/meta.dart'; import '../raw_editor.dart'; -import 'ime/on_delete.dart'; -import 'ime/on_insert.dart'; -import 'ime/on_non_update_text.dart'; -import 'ime/on_replace_method.dart'; +import 'ime/ime_internals.dart'; -mixin RawEditorStateTextInputClientMixin on EditorState implements DeltaTextInputClient { +mixin RawEditorStateTextInputClientMixin on EditorState + implements DeltaTextInputClient { TextInputConnection? _textInputConnection; TextEditingValue? __lastKnownRemoteTextEditingValue; @@ -103,9 +101,12 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements DeltaTextInpu if (_lastKnownRemoteTextEditingValue != null) { if (_lastKnownRemoteTextEditingValue!.selection.end > _lastKnownRemoteTextEditingValue!.text.length) { - _lastKnownRemoteTextEditingValue = _lastKnownRemoteTextEditingValue!.copyWith( - selection: _lastKnownRemoteTextEditingValue!.selection - .copyWith(extentOffset: _lastKnownRemoteTextEditingValue!.text.length)); + _lastKnownRemoteTextEditingValue = _lastKnownRemoteTextEditingValue! + .copyWith( + selection: _lastKnownRemoteTextEditingValue!.selection + .copyWith( + extentOffset: + _lastKnownRemoteTextEditingValue!.text.length)); } } _textInputConnection!.setEditingState(_lastKnownRemoteTextEditingValue!); @@ -115,8 +116,8 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements DeltaTextInpu // windows void _updateComposingRectIfNeeded() { - final composingRange = - _lastKnownRemoteTextEditingValue?.composing ?? textEditingValue.composing; + final composingRange = _lastKnownRemoteTextEditingValue?.composing ?? + textEditingValue.composing; if (hasConnection) { assert(mounted); if (composingRange.isValid) { @@ -138,10 +139,12 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements DeltaTextInpu renderEditor.selection.isCollapsed) { final currentTextPosition = TextPosition(offset: renderEditor.selection.baseOffset); - final caretRect = renderEditor.getLocalRectForCaret(currentTextPosition); + final caretRect = + renderEditor.getLocalRectForCaret(currentTextPosition); _textInputConnection!.setCaretRect(caretRect); } - SchedulerBinding.instance.addPostFrameCallback((_) => _updateCaretRectIfNeeded()); + SchedulerBinding.instance + .addPostFrameCallback((_) => _updateCaretRectIfNeeded()); } } @@ -192,7 +195,8 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements DeltaTextInpu // Start TextInputClient implementation @override - TextEditingValue? get currentTextEditingValue => _lastKnownRemoteTextEditingValue; + TextEditingValue? get currentTextEditingValue => + _lastKnownRemoteTextEditingValue; // autofill is not needed @override @@ -286,10 +290,11 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements DeltaTextInpu final currentTextPosition = TextPosition(offset: renderEditor.selection.baseOffset); - _startCaretRect = renderEditor.getLocalRectForCaret(currentTextPosition); + _startCaretRect = + renderEditor.getLocalRectForCaret(currentTextPosition); - _lastBoundedOffset = - _startCaretRect!.center - _floatingCursorOffset(currentTextPosition); + _lastBoundedOffset = _startCaretRect!.center - + _floatingCursorOffset(currentTextPosition); _lastTextPosition = currentTextPosition; renderEditor.setFloatingCursor( point.state, _lastBoundedOffset!, _lastTextPosition!); @@ -301,27 +306,31 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements DeltaTextInpu final rawCursorOffset = _startCaretRect!.center + centeredPoint - floatingCursorOffset; - final preferredLineHeight = renderEditor.preferredLineHeight(_lastTextPosition!); + final preferredLineHeight = + renderEditor.preferredLineHeight(_lastTextPosition!); _lastBoundedOffset = renderEditor.calculateBoundedFloatingCursorOffset( rawCursorOffset, preferredLineHeight, ); - _lastTextPosition = renderEditor.getPositionForOffset( - renderEditor.localToGlobal(_lastBoundedOffset! + floatingCursorOffset)); + _lastTextPosition = renderEditor.getPositionForOffset(renderEditor + .localToGlobal(_lastBoundedOffset! + floatingCursorOffset)); renderEditor.setFloatingCursor( point.state, _lastBoundedOffset!, _lastTextPosition!); final newSelection = TextSelection.collapsed( - offset: _lastTextPosition!.offset, affinity: _lastTextPosition!.affinity); + offset: _lastTextPosition!.offset, + affinity: _lastTextPosition!.affinity); // Setting selection as floating cursor moves will have scroll view // bring background cursor into view - renderEditor.onSelectionChanged(newSelection, SelectionChangedCause.forcePress); + renderEditor.onSelectionChanged( + newSelection, SelectionChangedCause.forcePress); break; case FloatingCursorDragState.End: // We skip animation if no update has happened. if (_lastTextPosition != null && _lastBoundedOffset != null) { floatingCursorResetController ..value = 0.0 - ..animateTo(1, duration: _floatingCursorResetTime, curve: Curves.decelerate); + ..animateTo(1, + duration: _floatingCursorResetTime, curve: Curves.decelerate); } break; } @@ -346,11 +355,13 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements DeltaTextInpu _lastBoundedOffset = null; } else { final lerpValue = floatingCursorResetController.value; - final lerpX = lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!; - final lerpY = lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!; + final lerpX = + lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!; + final lerpY = + lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!; - renderEditor.setFloatingCursor( - FloatingCursorDragState.Update, Offset(lerpX, lerpY), _lastTextPosition!, + renderEditor.setFloatingCursor(FloatingCursorDragState.Update, + Offset(lerpX, lerpY), _lastTextPosition!, resetLerpValue: lerpValue); } } @@ -376,7 +387,8 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements DeltaTextInpu void updateLastKnownRemoteTextEditingValueWithDeltas(TextEditingDelta delta) { // Apply the deltas to the previous platform-side IME value, to find out // what the platform thinks the IME value is - _lastKnownRemoteTextEditingValue = delta.apply(_lastKnownRemoteTextEditingValue!); + _lastKnownRemoteTextEditingValue = + delta.apply(_lastKnownRemoteTextEditingValue!); } void _updateSizeAndTransform() { @@ -386,7 +398,8 @@ mixin RawEditorStateTextInputClientMixin on EditorState implements DeltaTextInpu final size = renderEditor.size; final transform = renderEditor.getTransformTo(null); _textInputConnection?.setEditableSizeAndTransform(size, transform); - SchedulerBinding.instance.addPostFrameCallback((_) => _updateSizeAndTransform()); + SchedulerBinding.instance + .addPostFrameCallback((_) => _updateSizeAndTransform()); } } }