Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 17 additions & 1 deletion lib/src/document/history.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,21 @@ class History {
///record delay
final int interval;

bool _forceMergeNext = false;

bool get forceMergeNext => _forceMergeNext;

/// Forces the next change to merge into the last undo step.
/// Automatically resets to false after the current synchronous operations finish.
set forceMergeNext(bool value) {
_forceMergeNext = value;
if (value) {
Future.microtask(() {
_forceMergeNext = false;
});
}
}

void handleDocChange(DocChange docChange) {
if (ignoreChange) return;
if (!userOnly || docChange.source == ChangeSource.local) {
Expand All @@ -51,7 +66,8 @@ class History {
var undoDelta = change.invert(before);
final timeStamp = DateTime.now().millisecondsSinceEpoch;

if (lastRecorded + interval > timeStamp && stack.undo.isNotEmpty) {
if ((forceMergeNext || lastRecorded + interval > timeStamp) &&
stack.undo.isNotEmpty) {
final lastDelta = stack.undo.removeLast();
undoDelta = undoDelta.compose(lastDelta);
} else {
Expand Down
12 changes: 12 additions & 0 deletions lib/src/editor/config/editor_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class QuillEditorConfig {
this.placeholder,
this.checkBoxReadOnly,
this.disableClipboard = false,
this.mergeImeCompositionHistory = false,
this.textSelectionThemeData,
this.showCursor,
this.paintCursorAboveText,
Expand Down Expand Up @@ -190,6 +191,14 @@ class QuillEditorConfig {
/// Defaults to `false`.
final bool disableClipboard;

/// Whether to merge the IME composing text (uncommitted text) into a single
/// undo/redo history step.
///
/// When set to `true`, typing Pinyin or other IME candidates will be grouped
/// together, preventing the undo stack from recording every single keystroke.
/// Defaults to `false`.
final bool mergeImeCompositionHistory;

/// Whether this editor should create a scrollable container for its content.
///
/// When set to `true` the editor's height can be controlled by [minHeight],
Expand Down Expand Up @@ -481,6 +490,7 @@ class QuillEditorConfig {
List<SpaceShortcutEvent>? spaceShortcutEvents,
bool? checkBoxReadOnly,
bool? disableClipboard,
bool? mergeImeCompositionHistory,
bool? scrollable,
double? scrollBottomInset,
bool? enableAlwaysIndentOnTab,
Expand Down Expand Up @@ -541,6 +551,8 @@ class QuillEditorConfig {
spaceShortcutEvents: spaceShortcutEvents ?? this.spaceShortcutEvents,
checkBoxReadOnly: checkBoxReadOnly ?? this.checkBoxReadOnly,
disableClipboard: disableClipboard ?? this.disableClipboard,
mergeImeCompositionHistory:
mergeImeCompositionHistory ?? this.mergeImeCompositionHistory,
scrollable: scrollable ?? this.scrollable,
onKeyPressed: onKeyPressed ?? this.onKeyPressed,
scrollBottomInset: scrollBottomInset ?? this.scrollBottomInset,
Expand Down
1 change: 1 addition & 0 deletions lib/src/editor/editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ class QuillEditorState extends State<QuillEditor>
readOnly: controller.readOnly,
checkBoxReadOnly: config.checkBoxReadOnly,
disableClipboard: config.disableClipboard,
mergeImeCompositionHistory: config.mergeImeCompositionHistory,
placeholder: config.placeholder,
onLaunchUrl: config.onLaunchUrl,
contextMenuBuilder: showSelectionToolbar
Expand Down
9 changes: 9 additions & 0 deletions lib/src/editor/raw_editor/config/raw_editor_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class QuillRawEditorConfig {
this.readOnly = false,
this.checkBoxReadOnly,
this.disableClipboard = false,
this.mergeImeCompositionHistory = false,
this.placeholder,
this.onLaunchUrl,
this.contextMenuBuilder = defaultContextMenuBuilder,
Expand Down Expand Up @@ -197,6 +198,14 @@ class QuillRawEditorConfig {
/// Defaults to `false`.
final bool disableClipboard;

/// Whether to merge the IME composing text (uncommitted text) into a single
/// undo/redo history step.
///
/// When set to `true`, typing Pinyin or other IME candidates will be grouped
/// together, preventing the undo stack from recording every single keystroke.
/// Defaults to `false`.
final bool mergeImeCompositionHistory;

final String? placeholder;

/// Callback which is triggered when the user wants to open a URL from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ mixin RawEditorStateTextInputClientMixin on EditorState
bool get hasConnection =>
_textInputConnection != null && _textInputConnection!.attached;

/// Indicates if the IME was composing in the previous update.
/// Used by [QuillEditorConfig.mergeImeCompositionHistory] to merge
/// intermediate keystrokes (e.g., Pinyin) into a single undo step.
bool _wasComposing = false;

/// Opens or closes input connection based on the current state of
/// [focusNode] and [value].
void openOrCloseConnection() {
Expand Down Expand Up @@ -213,6 +218,14 @@ mixin RawEditorStateTextInputClientMixin on EditorState
return;
}

if (widget.config.mergeImeCompositionHistory) {
// Handle IME composition history.
// If the user was composing (e.g., typing Pinyin) in the previous step,
// force the upcoming document change to merge into the same undo step.
widget.controller.document.history.forceMergeNext = _wasComposing;
_wasComposing = value.composing.isValid;
}

// Check if only composing range changed.
if (_lastKnownRemoteTextEditingValue!.text == value.text &&
_lastKnownRemoteTextEditingValue!.selection == value.selection) {
Expand Down