Skip to content

Chore(breaking changes): Replace TextInputClient with DeltaTextInputClient #2510

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 44 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
3297a1f
Chore: improved input client service
Feb 21, 2025
b812460
Chore: minor changes
Mar 11, 2025
7685b57
Chore: renamed input client to the old name of the text input clinet
Mar 11, 2025
5a3dab3
Chore: removed unnecessary print
Mar 12, 2025
e039446
Chore: moved internal editing extensions and change List deltas to a …
Mar 12, 2025
bcacc27
Chore: removed unused import
Mar 12, 2025
a4d567a
doc: update controller length extension method deprecation message (#…
realth000 Feb 20, 2025
1a287b7
chore: update GitHub bug template to require the package version input
EchoEllet Feb 20, 2025
54ccff3
Focus and open context menu on right click if unfocused (#2477)
tjarvstrand Feb 21, 2025
dfb7bbc
Expose Rule type so that Document.setCustomRules can be used (#2484)
tjarvstrand Feb 22, 2025
eb91d24
feat: Enable BoxDecoration for DefaultTextBlockStyle of header Attrib…
satotoshitaka11 Feb 24, 2025
8d63996
fix: unpredictable endless loop of '_handleFocusChanged' in the phase…
chaosue Mar 3, 2025
62ecf21
chore(release): prepare to publish 11.1.0
EchoEllet Mar 10, 2025
24e1305
Chore: added change to CHANGELOG
Mar 12, 2025
3a147cf
Merge branch 'master' into improve_input_client
CatHood0 Mar 12, 2025
9000119
Chore: minor changes
Mar 12, 2025
1518987
Chore: removed debounce timer since it does not needed in our impleme…
Mar 12, 2025
50a1499
Chore: removed commented neccesary schedulers
Mar 12, 2025
d998e45
Chore: removed unnecessary _updateComposing method
Mar 12, 2025
6ceef54
Fix: buggy behavior of the caret when try to delete characters on and…
Mar 12, 2025
fd66368
Chore: used old implementation for web since the new one does not wor…
Mar 12, 2025
e98e16d
Chore: removed composing range validation to get the cursorPosition f…
Mar 12, 2025
9202560
Chore: replaced use of dart:io to use platform utils
Mar 12, 2025
25d07ad
Chore: fix buggy behavior in onDelete on web browsers
Mar 12, 2025
9ff127d
Chore: removed formatters since them are not doing nothing and cause …
Mar 12, 2025
70ccec1
Chore: replaced get selection from controller to use the one from the…
Mar 12, 2025
b5ee9f4
Chore: removed unused import
Mar 12, 2025
aad0b56
Fix: removed unused imports and deleted import for formatters
Mar 12, 2025
ea112f1
Chore: removed unnecessary double check for use CharacterShortcutEvet…
Mar 12, 2025
fa3dbc3
Chore: updated change title in CHANGELOG
Mar 12, 2025
f12327b
Chore: update pubspec.lock from example
Mar 13, 2025
b1fbe0a
Chore: use insertion.insertionOffset instead selection from new value
Mar 13, 2025
2e1de1d
merge branch 'master' into better_soft_keyboard_support
Mar 13, 2025
f59c58b
Chore: improved perfomance of ime operations
Mar 13, 2025
155491f
Chore: removed unused imports in text_input_mixin
Mar 13, 2025
cf46f8c
Merge branch 'master' into better_soft_keyboard_support
EchoEllet Mar 19, 2025
1d0aa63
Chore: implemented delta_text_input_client instead diffing string cha…
Mar 29, 2025
2936b93
Chore: format
Mar 29, 2025
a9f2b5b
Chore: removed TextInputClient by recommendation of flutter docs
Mar 29, 2025
44bdd3d
Chore: removed use of updateEditingValue by recommendation of flutter…
Mar 29, 2025
61d753d
Merge 'master' into better_soft_keyboard_support
Mar 29, 2025
924a27b
Chore: added method for apply deltas to the last known editing value
Mar 29, 2025
74287ae
Fix: missed method name change
Mar 29, 2025
dc438bf
Chore: moved ime helpers to be part of ime_internals file
Mar 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- Replace `TextInputClient` with `DeltaTextInputClient` [#2509](https://github.com/singerdmx/flutter-quill/pull/2509).

## [11.2.0] - 2025-03-26

### Added
Expand Down
8 changes: 8 additions & 0 deletions lib/src/common/utils/platform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion lib/src/delta/delta_diff.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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]) ==
Expand Down
13 changes: 13 additions & 0 deletions lib/src/editor/raw_editor/input/ime/ime_internals.dart
Original file line number Diff line number Diff line change
@@ -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';
22 changes: 22 additions & 0 deletions lib/src/editor/raw_editor/input/ime/on_delete.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
part of 'ime_internals.dart';

void onDelete(
TextEditingDeltaDeletion deletion,
QuillController controller,
) {
final start = deletion.deletedRange.start;
final length = deletion.deletedRange.end - start;
controller.replaceText(
start,
length,
'',
TextSelection.collapsed(
offset: deletion.selection.baseOffset.nonNegative,
affinity: controller.selection.affinity,
),
);
}

extension on int {
int get nonNegative => this < 0 ? 0 : this;
}
30 changes: 30 additions & 0 deletions lib/src/editor/raw_editor/input/ime/on_insert.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
part of 'ime_internals.dart';

void onInsert(
TextEditingDeltaInsertion insertion,
QuillController controller,
List<CharacterShortcutEvent> characterShortcutEvents,
) {
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(
insertion.insertionOffset,
selection.extentOffset - selection.baseOffset,
insertionText,
TextSelection.collapsed(
offset: insertion.insertionOffset + insertionText.length,
affinity: selection.affinity,
),
);
}
24 changes: 24 additions & 0 deletions lib/src/editor/raw_editor/input/ime/on_non_update_text.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
part of 'ime_internals.dart';

void onNonTextUpdate(
TextEditingDeltaNonTextUpdate nonTextUpdate,
QuillController controller,
) {
final effectiveSelection = nonTextUpdate.selection;
// when typing characters with CJK IME on Windows, a non-text update is sent
// with the selection range.
if (isWindowsApp) {
if (nonTextUpdate.composing == TextRange.empty &&
nonTextUpdate.selection.isCollapsed) {
controller.updateSelection(
effectiveSelection,
ChangeSource.local,
);
}
return;
}
controller.updateSelection(
effectiveSelection,
ChangeSource.local,
);
}
37 changes: 37 additions & 0 deletions lib/src/editor/raw_editor/input/ime/on_replace_method.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
part of 'ime_internals.dart';

void onReplace(
TextEditingDeltaReplacement replacement,
QuillController controller,
List<CharacterShortcutEvent> characterShortcutEvents,
) {
// delete the selection
final selection = controller.selection;

final textReplacement = replacement.replacementText;

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,
);
}
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,
),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:meta/meta.dart';

import '../../delta/delta_diff.dart';
import '../../document/document.dart';
import '../editor.dart';
import 'raw_editor.dart';
import '../raw_editor.dart';
import 'ime/ime_internals.dart';

mixin RawEditorStateTextInputClientMixin on EditorState
implements TextInputClient {
implements DeltaTextInputClient {
TextInputConnection? _textInputConnection;
TextEditingValue? __lastKnownRemoteTextEditingValue;

Expand Down Expand Up @@ -82,6 +79,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: createKeyboardAppearance(),
Expand Down Expand Up @@ -115,6 +114,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState
_textInputConnection!.show();
}

// windows
void _updateComposingRectIfNeeded() {
final composingRange = _lastKnownRemoteTextEditingValue?.composing ??
textEditingValue.composing;
Expand All @@ -131,6 +131,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState
}
}

// macos
void _updateCaretRectIfNeeded() {
if (hasConnection) {
if (!dirty &&
Expand Down Expand Up @@ -202,43 +203,43 @@ mixin RawEditorStateTextInputClientMixin on EditorState
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;
}
void updateEditingValue(TextEditingValue value) {}

// 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;
@override
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
if (!shouldCreateInputConnection || textEditingDeltas.isEmpty) {
return;
}
_apply(textEditingDeltas);
}

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,
);
void _apply(List<TextEditingDelta> deltas) {
for (final delta in deltas) {
// updates _lastKnownRemoteTextEditingValue to avoid issues
updateLastKnownRemoteTextEditingValueWithDeltas(delta);
if (delta is TextEditingDeltaInsertion) {
onInsert(
delta,
widget.controller,
widget.config.characterShortcutEvents,
);
} else if (delta is TextEditingDeltaDeletion) {
onDelete(
delta,
widget.controller,
);
} else if (delta is TextEditingDeltaReplacement) {
onReplace(
delta,
widget.controller,
widget.config.characterShortcutEvents,
);
} else if (delta is TextEditingDeltaNonTextUpdate) {
onNonTextUpdate(
delta,
widget.controller,
);
}
}
}

Expand Down Expand Up @@ -381,6 +382,15 @@ mixin RawEditorStateTextInputClientMixin on EditorState
_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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -26,7 +25,6 @@ class EditorKeyboardShortcuts extends StatelessWidget {
required this.controller,
required this.readOnly,
required this.enableAlwaysIndentOnTab,
required this.characterEvents,
required this.spaceEvents,
this.onKeyPressed,
this.customShortcuts,
Expand All @@ -39,7 +37,6 @@ class EditorKeyboardShortcuts extends StatelessWidget {
final QuillController controller;
@experimental
final KeyEventResult? Function(KeyEvent event, Node? node)? onKeyPressed;
final List<CharacterShortcutEvent> characterEvents;
final List<SpaceShortcutEvent> spaceEvents;
final Map<ShortcutActivator, Intent>? customShortcuts;
final Map<Type, Action<Intent>>? customActions;
Expand Down Expand Up @@ -97,17 +94,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;
Expand Down
3 changes: 1 addition & 2 deletions lib/src/editor/raw_editor/raw_editor_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ 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';
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
Expand Down Expand Up @@ -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,
Expand Down
Loading