From 7ee58bf46de258b7bd072e4b80799c96f5fd46cc Mon Sep 17 00:00:00 2001 From: X Code Date: Fri, 10 Dec 2021 07:28:48 -0800 Subject: [PATCH] Upgrade to 2.8 (#519) --- lib/src/utils/diff_delta.dart | 29 -- lib/src/widgets/editor.dart | 157 ++++++-- lib/src/widgets/keyboard_listener.dart | 129 ------ lib/src/widgets/raw_editor.dart | 105 +++-- .../raw_editor_state_keyboard_mixin.dart | 368 ------------------ ...editor_state_selection_delegate_mixin.dart | 130 +------ ..._editor_state_text_input_client_mixin.dart | 9 +- lib/widgets/keyboard_listener.dart | 3 - .../raw_editor_state_keyboard_mixin.dart | 3 - 9 files changed, 222 insertions(+), 711 deletions(-) delete mode 100644 lib/src/widgets/keyboard_listener.dart delete mode 100644 lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart delete mode 100644 lib/widgets/keyboard_listener.dart delete mode 100644 lib/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart diff --git a/lib/src/utils/diff_delta.dart b/lib/src/utils/diff_delta.dart index f44a1a09..cf0e5c63 100644 --- a/lib/src/utils/diff_delta.dart +++ b/lib/src/utils/diff_delta.dart @@ -2,35 +2,6 @@ import 'dart:math' as math; import '../models/quill_delta.dart'; -const Set WHITE_SPACE = { - 0x9, - 0xA, - 0xB, - 0xC, - 0xD, - 0x1C, - 0x1D, - 0x1E, - 0x1F, - 0x20, - 0xA0, - 0x1680, - 0x2000, - 0x2001, - 0x2002, - 0x2003, - 0x2004, - 0x2005, - 0x2006, - 0x2007, - 0x2008, - 0x2009, - 0x200A, - 0x202F, - 0x205F, - 0x3000 -}; - // Diff between two texts - old text and new text class Diff { Diff(this.start, this.deleted, this.inserted); diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 17ef54a8..94869f8d 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -47,13 +47,10 @@ const linkPrefixes = [ 'http' ]; -abstract class EditorState extends State { +abstract class EditorState extends State + implements TextSelectionDelegate { ScrollController get scrollController; - TextEditingValue getTextEditingValue(); - - void setTextEditingValue(TextEditingValue value, SelectionChangedCause cause); - RenderEditor? getRenderEditor(); EditorTextSelectionOverlay? getSelectionOverlay(); @@ -64,15 +61,11 @@ abstract class EditorState extends State { bool showToolbar(); - void hideToolbar(); - void requestKeyboard(); - - bool get readOnly; } /// Base interface for editable render objects. -abstract class RenderAbstractEditor { +abstract class RenderAbstractEditor implements TextLayoutMetrics { TextSelection selectWordAtPosition(TextPosition position); TextSelection selectLineAtPosition(TextPosition position); @@ -684,6 +677,7 @@ const EdgeInsets _kFloatingCaretSizeIncrease = EdgeInsets.symmetric(horizontal: 0.5, vertical: 1); class RenderEditor extends RenderEditableContainerBox + with RelayoutWhenSystemFontsChangeMixin implements RenderAbstractEditor { RenderEditor( ViewportOffset? offset, @@ -985,15 +979,8 @@ class RenderEditor extends RenderEditableContainerBox @override TextSelection selectWordAtPosition(TextPosition position) { - final child = childAtPosition(position); - final nodeOffset = child.getContainer().offset; - final localPosition = TextPosition( - offset: position.offset - nodeOffset, affinity: position.affinity); - final localWord = child.getWordBoundary(localPosition); - final word = TextRange( - start: localWord.start + nodeOffset, - end: localWord.end + nodeOffset, - ); + final word = getWordBoundary(position); + // When long-pressing past the end of the text, we want a collapsed cursor. if (position.offset >= word.end) { return TextSelection.fromPosition(position); } @@ -1002,16 +989,9 @@ class RenderEditor extends RenderEditableContainerBox @override TextSelection selectLineAtPosition(TextPosition position) { - final child = childAtPosition(position); - final nodeOffset = child.getContainer().offset; - final localPosition = TextPosition( - offset: position.offset - nodeOffset, affinity: position.affinity); - final localLineRange = child.getLineBoundary(localPosition); - final line = TextRange( - start: localLineRange.start + nodeOffset, - end: localLineRange.end + nodeOffset, - ); + final line = getLineAtOffset(position); + // When long-pressing past the end of the text, we want a collapsed cursor. if (position.offset >= line.end) { return TextSelection.fromPosition(position); } @@ -1266,7 +1246,126 @@ class RenderEditor extends RenderEditableContainerBox _floatingCursorPainter.paint(context.canvas); } -// End floating cursor + // End floating cursor + + // Start TextLayoutMetrics implementation + + /// Return a [TextSelection] containing the line of the given [TextPosition]. + @override + TextSelection getLineAtOffset(TextPosition position) { + final child = childAtPosition(position); + final nodeOffset = child.getContainer().offset; + final localPosition = TextPosition( + offset: position.offset - nodeOffset, affinity: position.affinity); + final localLineRange = child.getLineBoundary(localPosition); + final line = TextRange( + start: localLineRange.start + nodeOffset, + end: localLineRange.end + nodeOffset, + ); + return TextSelection(baseOffset: line.start, extentOffset: line.end); + } + + @override + TextRange getWordBoundary(TextPosition position) { + final child = childAtPosition(position); + final nodeOffset = child.getContainer().offset; + final localPosition = TextPosition( + offset: position.offset - nodeOffset, affinity: position.affinity); + final localWord = child.getWordBoundary(localPosition); + return TextRange( + start: localWord.start + nodeOffset, + end: localWord.end + nodeOffset, + ); + } + + /// Returns the TextPosition above the given offset into the text. + /// + /// If the offset is already on the first line, the offset of the first + /// character will be returned. + @override + TextPosition getTextPositionAbove(TextPosition position) { + final child = childAtPosition(position); + final localPosition = TextPosition( + offset: position.offset - child.getContainer().documentOffset); + + var newPosition = child.getPositionAbove(localPosition); + + if (newPosition == null) { + // There was no text above in the current child, check the direct + // sibling. + final sibling = childBefore(child); + if (sibling == null) { + // reached beginning of the document, move to the + // first character + newPosition = const TextPosition(offset: 0); + } else { + final caretOffset = child.getOffsetForCaret(localPosition); + final testPosition = + TextPosition(offset: sibling.getContainer().length - 1); + final testOffset = sibling.getOffsetForCaret(testPosition); + final finalOffset = Offset(caretOffset.dx, testOffset.dy); + final siblingPosition = sibling.getPositionForOffset(finalOffset); + newPosition = TextPosition( + offset: + sibling.getContainer().documentOffset + siblingPosition.offset); + } + } else { + newPosition = TextPosition( + offset: child.getContainer().documentOffset + newPosition.offset); + } + return newPosition; + } + + /// Returns the TextPosition below the given offset into the text. + /// + /// If the offset is already on the last line, the offset of the last + /// character will be returned. + @override + TextPosition getTextPositionBelow(TextPosition position) { + final child = childAtPosition(position); + final localPosition = TextPosition( + offset: position.offset - child.getContainer().documentOffset); + + var newPosition = child.getPositionBelow(localPosition); + + if (newPosition == null) { + // There was no text above in the current child, check the direct + // sibling. + final sibling = childAfter(child); + if (sibling == null) { + // reached beginning of the document, move to the + // last character + newPosition = TextPosition(offset: document.length - 1); + } else { + final caretOffset = child.getOffsetForCaret(localPosition); + const testPosition = TextPosition(offset: 0); + final testOffset = sibling.getOffsetForCaret(testPosition); + final finalOffset = Offset(caretOffset.dx, testOffset.dy); + final siblingPosition = sibling.getPositionForOffset(finalOffset); + newPosition = TextPosition( + offset: + sibling.getContainer().documentOffset + siblingPosition.offset); + } + } else { + newPosition = TextPosition( + offset: child.getContainer().documentOffset + newPosition.offset); + } + return newPosition; + } + + // End TextLayoutMetrics implementation + + @override + void systemFontsDidChange() { + super.systemFontsDidChange(); + markNeedsLayout(); + } + + void debugAssertLayoutUpToDate() { + // no-op? + // this assert was added by Flutter TextEditingActionTarge + // so we have to comply here. + } } class EditableContainerParentData diff --git a/lib/src/widgets/keyboard_listener.dart b/lib/src/widgets/keyboard_listener.dart deleted file mode 100644 index 4e755fdf..00000000 --- a/lib/src/widgets/keyboard_listener.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -//fixme workaround flutter MacOS issue https://github.com/flutter/flutter/issues/75595 -extension _LogicalKeyboardKeyCaseExt on LogicalKeyboardKey { - static const _kUpperToLowerDist = 0x20; - static final _kLowerCaseA = LogicalKeyboardKey.keyA.keyId; - static final _kLowerCaseZ = LogicalKeyboardKey.keyZ.keyId; - - LogicalKeyboardKey toUpperCase() { - if (keyId < _kLowerCaseA || keyId > _kLowerCaseZ) return this; - return LogicalKeyboardKey(keyId - _kUpperToLowerDist); - } -} - -enum InputShortcut { CUT, COPY, PASTE, SELECT_ALL, UNDO, REDO } - -typedef CursorMoveCallback = void Function( - LogicalKeyboardKey key, bool wordModifier, bool lineModifier, bool shift); -typedef InputShortcutCallback = void Function(InputShortcut? shortcut); -typedef OnDeleteCallback = void Function(bool forward); - -class KeyboardEventHandler { - KeyboardEventHandler(this.onCursorMove, this.onShortcut, this.onDelete); - - final CursorMoveCallback onCursorMove; - final InputShortcutCallback onShortcut; - final OnDeleteCallback onDelete; - - static final Set _moveKeys = { - LogicalKeyboardKey.arrowRight, - LogicalKeyboardKey.arrowLeft, - LogicalKeyboardKey.arrowUp, - LogicalKeyboardKey.arrowDown, - }; - - static final Set _shortcutKeys = { - LogicalKeyboardKey.keyA, - LogicalKeyboardKey.keyC, - LogicalKeyboardKey.keyV, - LogicalKeyboardKey.keyX, - LogicalKeyboardKey.keyZ.toUpperCase(), - LogicalKeyboardKey.keyZ, - LogicalKeyboardKey.delete, - LogicalKeyboardKey.backspace, - }; - - static final Set _nonModifierKeys = { - ..._shortcutKeys, - ..._moveKeys, - }; - - static final Set _modifierKeys = { - LogicalKeyboardKey.shift, - LogicalKeyboardKey.control, - LogicalKeyboardKey.alt, - }; - - static final Set _macOsModifierKeys = - { - LogicalKeyboardKey.shift, - LogicalKeyboardKey.meta, - LogicalKeyboardKey.alt, - }; - - static final Set _interestingKeys = { - ..._modifierKeys, - ..._macOsModifierKeys, - ..._nonModifierKeys, - }; - - static final Map _keyToShortcut = { - LogicalKeyboardKey.keyX: InputShortcut.CUT, - LogicalKeyboardKey.keyC: InputShortcut.COPY, - LogicalKeyboardKey.keyV: InputShortcut.PASTE, - LogicalKeyboardKey.keyA: InputShortcut.SELECT_ALL, - }; - - KeyEventResult handleRawKeyEvent(RawKeyEvent event) { - if (kIsWeb) { - // On web platform, we ignore the key because it's already processed. - return KeyEventResult.ignored; - } - - if (event is! RawKeyDownEvent) { - return KeyEventResult.ignored; - } - - final keysPressed = - LogicalKeyboardKey.collapseSynonyms(RawKeyboard.instance.keysPressed); - final key = event.logicalKey; - final isMacOS = event.data is RawKeyEventDataMacOs; - if (!_nonModifierKeys.contains(key) || - keysPressed - .difference(isMacOS ? _macOsModifierKeys : _modifierKeys) - .length > - 1 || - keysPressed.difference(_interestingKeys).isNotEmpty) { - return KeyEventResult.ignored; - } - - final isShortcutModifierPressed = - isMacOS ? event.isMetaPressed : event.isControlPressed; - - if (_moveKeys.contains(key)) { - onCursorMove( - key, - isMacOS ? event.isAltPressed : event.isControlPressed, - isMacOS ? event.isMetaPressed : event.isAltPressed, - event.isShiftPressed); - } else if (isShortcutModifierPressed && (_shortcutKeys.contains(key))) { - if (key == LogicalKeyboardKey.keyZ || - key == LogicalKeyboardKey.keyZ.toUpperCase()) { - onShortcut( - event.isShiftPressed ? InputShortcut.REDO : InputShortcut.UNDO); - } else { - onShortcut(_keyToShortcut[key]); - } - } else if (key == LogicalKeyboardKey.delete) { - onDelete(true); - } else if (key == LogicalKeyboardKey.backspace) { - onDelete(false); - } else { - return KeyEventResult.ignored; - } - return KeyEventResult.handled; - } -} diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index eef259f6..daa9563b 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -21,10 +21,8 @@ import 'cursor.dart'; import 'default_styles.dart'; import 'delegate.dart'; import 'editor.dart'; -import 'keyboard_listener.dart'; import 'proxy.dart'; import 'quill_single_child_scroll_view.dart'; -import 'raw_editor/raw_editor_state_keyboard_mixin.dart'; import 'raw_editor/raw_editor_state_selection_delegate_mixin.dart'; import 'raw_editor/raw_editor_state_text_input_client_mixin.dart'; import 'text_block.dart'; @@ -106,13 +104,11 @@ class RawEditorState extends EditorState AutomaticKeepAliveClientMixin, WidgetsBindingObserver, TickerProviderStateMixin, - RawEditorStateKeyboardMixin, + TextEditingActionTarget, RawEditorStateTextInputClientMixin, RawEditorStateSelectionDelegateMixin { final GlobalKey _editorKey = GlobalKey(); - // Keyboard - late KeyboardEventHandler _keyboardListener; KeyboardVisibilityController? _keyboardVisibilityController; StreamSubscription? _keyboardVisibilitySubscription; bool _keyboardVisible = false; @@ -370,13 +366,6 @@ class RawEditorState extends EditorState _floatingCursorResetController = AnimationController(vsync: this); _floatingCursorResetController.addListener(onFloatingCursorResetTick); - // Keyboard - _keyboardListener = KeyboardEventHandler( - handleCursorMovement, - handleShortcut, - handleDelete, - ); - if (defaultTargetPlatform == TargetPlatform.windows || defaultTargetPlatform == TargetPlatform.macOS || defaultTargetPlatform == TargetPlatform.linux || @@ -394,8 +383,7 @@ class RawEditorState extends EditorState }); } - _focusAttachment = widget.focusNode.attach(context, - onKey: (node, event) => _keyboardListener.handleRawKeyEvent(event)); + _focusAttachment = widget.focusNode.attach(context); widget.focusNode.addListener(_handleFocusChanged); } @@ -440,8 +428,7 @@ class RawEditorState extends EditorState if (widget.focusNode != oldWidget.focusNode) { oldWidget.focusNode.removeListener(_handleFocusChanged); _focusAttachment?.detach(); - _focusAttachment = widget.focusNode.attach(context, - onKey: (node, event) => _keyboardListener.handleRawKeyEvent(event)); + _focusAttachment = widget.focusNode.attach(context); widget.focusNode.addListener(_handleFocusChanged); updateKeepAlive(); } @@ -634,11 +621,6 @@ class RawEditorState extends EditorState return _editorKey.currentContext?.findRenderObject() as RenderEditor?; } - @override - TextEditingValue getTextEditingValue() { - return widget.controller.plainTextEditingValue; - } - @override void requestKeyboard() { if (_hasFocus) { @@ -652,11 +634,16 @@ class RawEditorState extends EditorState @override void setTextEditingValue( TextEditingValue value, SelectionChangedCause cause) { - if (value.text == textEditingValue.text) { - widget.controller.updateSelection(value.selection, ChangeSource.LOCAL); - } else { - _setEditingValue(value); + if (value == textEditingValue) { + return; } + textEditingValue = value; + userUpdateTextEditingValue(value, cause); + } + + @override + void debugAssertLayoutUpToDate() { + getRenderEditor()!.debugAssertLayoutUpToDate(); } // set editing value from clipboard for mobile @@ -731,12 +718,80 @@ class RawEditorState extends EditorState return true; } + @override + void copySelection(SelectionChangedCause cause) { + // Copied straight from EditableTextState + super.copySelection(cause); + if (cause == SelectionChangedCause.toolbar) { + bringIntoView(textEditingValue.selection.extent); + hideToolbar(false); + + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + break; + case TargetPlatform.macOS: + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + // Collapse the selection and hide the toolbar and handles. + userUpdateTextEditingValue( + TextEditingValue( + text: textEditingValue.text, + selection: TextSelection.collapsed( + offset: textEditingValue.selection.end), + ), + SelectionChangedCause.toolbar, + ); + break; + } + } + } + + @override + void cutSelection(SelectionChangedCause cause) { + // Copied straight from EditableTextState + super.cutSelection(cause); + if (cause == SelectionChangedCause.toolbar) { + bringIntoView(textEditingValue.selection.extent); + hideToolbar(); + } + } + + @override + Future pasteText(SelectionChangedCause cause) async { + // Copied straight from EditableTextState + super.pasteText(cause); // ignore: unawaited_futures + if (cause == SelectionChangedCause.toolbar) { + bringIntoView(textEditingValue.selection.extent); + hideToolbar(); + } + } + + @override + void selectAll(SelectionChangedCause cause) { + // Copied straight from EditableTextState + super.selectAll(cause); + if (cause == SelectionChangedCause.toolbar) { + bringIntoView(textEditingValue.selection.extent); + } + } + @override bool get wantKeepAlive => widget.focusNode.hasFocus; + @override + bool get obscureText => false; + + @override + bool get selectionEnabled => widget.enableInteractiveSelection; + @override bool get readOnly => widget.readOnly; + @override + TextLayoutMetrics get textLayoutMetrics => getRenderEditor()!; + @override AnimationController get floatingCursorResetController => _floatingCursorResetController; diff --git a/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart deleted file mode 100644 index 05dc0057..00000000 --- a/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart +++ /dev/null @@ -1,368 +0,0 @@ -import 'dart:ui'; - -import 'package:characters/characters.dart'; -import 'package:flutter/services.dart'; - -import '../../models/documents/document.dart'; -import '../../utils/diff_delta.dart'; -import '../editor.dart'; -import '../keyboard_listener.dart'; - -mixin RawEditorStateKeyboardMixin on EditorState { - // Holds the last cursor location the user selected in the case the user tries - // to select vertically past the end or beginning of the field. If they do, - // then we need to keep the old cursor location so that we can go back to it - // if they change their minds. Only used for moving selection up and down in a - // multiline text field when selecting using the keyboard. - int _cursorResetLocation = -1; - - // Whether we should reset the location of the cursor in the case the user - // tries to select vertically past the end or beginning of the field. If they - // do, then we need to keep the old cursor location so that we can go back to - // it if they change their minds. Only used for resetting selection up and - // down in a multiline text field when selecting using the keyboard. - bool _wasSelectingVerticallyWithKeyboard = false; - - void handleCursorMovement( - LogicalKeyboardKey key, - bool wordModifier, - bool lineModifier, - bool shift, - ) { - if (wordModifier && lineModifier) { - // If both modifiers are down, nothing happens on any of the platforms. - return; - } - final selection = widget.controller.selection; - - var newSelection = widget.controller.selection; - - final plainText = getTextEditingValue().text; - - final rightKey = key == LogicalKeyboardKey.arrowRight, - leftKey = key == LogicalKeyboardKey.arrowLeft, - upKey = key == LogicalKeyboardKey.arrowUp, - downKey = key == LogicalKeyboardKey.arrowDown; - - if ((rightKey || leftKey) && !(rightKey && leftKey)) { - newSelection = _jumpToBeginOrEndOfWord(newSelection, wordModifier, - leftKey, rightKey, plainText, lineModifier, shift); - } - - if (downKey || upKey) { - newSelection = _handleMovingCursorVertically( - upKey, downKey, shift, selection, newSelection, plainText); - } - - if (!shift) { - newSelection = - _placeCollapsedSelection(selection, newSelection, leftKey, rightKey); - } - - widget.controller.updateSelection(newSelection, ChangeSource.LOCAL); - } - - // Handles shortcut functionality including cut, copy, paste and select all - // using control/command + (X, C, V, A). - // TODO: Add support for formatting shortcuts: Cmd+B (bold), Cmd+I (italic) - // set editing value from clipboard for web - Future handleShortcut(InputShortcut? shortcut) async { - final selection = widget.controller.selection; - final plainText = getTextEditingValue().text; - if (shortcut == InputShortcut.COPY) { - if (!selection.isCollapsed) { - await Clipboard.setData( - ClipboardData(text: selection.textInside(plainText))); - } - return; - } - if (shortcut == InputShortcut.UNDO) { - if (widget.controller.hasUndo) { - widget.controller.undo(); - } - return; - } - if (shortcut == InputShortcut.REDO) { - if (widget.controller.hasRedo) { - widget.controller.redo(); - } - return; - } - if (shortcut == InputShortcut.CUT && !widget.readOnly) { - if (!selection.isCollapsed) { - final data = selection.textInside(plainText); - await Clipboard.setData(ClipboardData(text: data)); - - widget.controller.replaceText( - selection.start, - data.length, - '', - TextSelection.collapsed(offset: selection.start), - ); - - setTextEditingValue( - TextEditingValue( - text: selection.textBefore(plainText) + - selection.textAfter(plainText), - selection: TextSelection.collapsed(offset: selection.start), - ), - SelectionChangedCause.keyboard); - } - return; - } - if (shortcut == InputShortcut.PASTE && !widget.readOnly) { - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data != null) { - widget.controller.replaceText( - selection.start, - selection.end - selection.start, - data.text, - TextSelection.collapsed(offset: selection.start + data.text!.length), - ); - } - return; - } - if (shortcut == InputShortcut.SELECT_ALL && - widget.enableInteractiveSelection) { - widget.controller.updateSelection( - selection.copyWith( - baseOffset: 0, - extentOffset: getTextEditingValue().text.length, - ), - ChangeSource.REMOTE); - return; - } - } - - void handleDelete(bool forward) { - final selection = widget.controller.selection; - final plainText = getTextEditingValue().text; - var cursorPosition = selection.start; - var textBefore = selection.textBefore(plainText); - var textAfter = selection.textAfter(plainText); - if (selection.isCollapsed) { - if (!forward && textBefore.isNotEmpty) { - final characterBoundary = - _previousCharacter(textBefore.length, textBefore, true); - textBefore = textBefore.substring(0, characterBoundary); - cursorPosition = characterBoundary; - } - if (forward && textAfter.isNotEmpty && textAfter != '\n') { - final deleteCount = _nextCharacter(0, textAfter, true); - textAfter = textAfter.substring(deleteCount); - } - } - final newSelection = TextSelection.collapsed(offset: cursorPosition); - final newText = textBefore + textAfter; - final size = plainText.length - newText.length; - if (size == 0) { - widget.controller.handleDelete(cursorPosition, forward); - } else { - widget.controller.replaceText(cursorPosition, size, '', newSelection); - } - } - - TextSelection _jumpToBeginOrEndOfWord( - TextSelection newSelection, - bool wordModifier, - bool leftKey, - bool rightKey, - String plainText, - bool lineModifier, - bool shift) { - if (wordModifier) { - if (leftKey) { - final textSelection = getRenderEditor()!.selectWordAtPosition( - TextPosition( - offset: _previousCharacter( - newSelection.extentOffset, plainText, false))); - return newSelection.copyWith(extentOffset: textSelection.baseOffset); - } - final textSelection = getRenderEditor()!.selectWordAtPosition( - TextPosition( - offset: - _nextCharacter(newSelection.extentOffset, plainText, false))); - return newSelection.copyWith(extentOffset: textSelection.extentOffset); - } else if (lineModifier) { - if (leftKey) { - final textSelection = getRenderEditor()!.selectLineAtPosition( - TextPosition( - offset: _previousCharacter( - newSelection.extentOffset, plainText, false))); - return newSelection.copyWith(extentOffset: textSelection.baseOffset); - } - final startPoint = newSelection.extentOffset; - if (startPoint < plainText.length) { - final textSelection = getRenderEditor()! - .selectLineAtPosition(TextPosition(offset: startPoint)); - return newSelection.copyWith(extentOffset: textSelection.extentOffset); - } - return newSelection; - } - - if (rightKey && newSelection.extentOffset < plainText.length) { - final nextExtent = - _nextCharacter(newSelection.extentOffset, plainText, true); - final distance = nextExtent - newSelection.extentOffset; - newSelection = newSelection.copyWith(extentOffset: nextExtent); - if (shift) { - _cursorResetLocation += distance; - } - return newSelection; - } - - if (leftKey && newSelection.extentOffset > 0) { - final previousExtent = - _previousCharacter(newSelection.extentOffset, plainText, true); - final distance = newSelection.extentOffset - previousExtent; - newSelection = newSelection.copyWith(extentOffset: previousExtent); - if (shift) { - _cursorResetLocation -= distance; - } - return newSelection; - } - return newSelection; - } - - /// Returns the index into the string of the next character boundary after the - /// given index. - /// - /// The character boundary is determined by the characters package, so - /// surrogate pairs and extended grapheme clusters are considered. - /// - /// The index must be between 0 and string.length, inclusive. If given - /// string.length, string.length is returned. - /// - /// Setting includeWhitespace to false will only return the index of non-space - /// characters. - int _nextCharacter(int index, String string, bool includeWhitespace) { - assert(index >= 0 && index <= string.length); - if (index == string.length) { - return string.length; - } - - var count = 0; - final remain = string.characters.skipWhile((currentString) { - if (count <= index) { - count += currentString.length; - return true; - } - if (includeWhitespace) { - return false; - } - return WHITE_SPACE.contains(currentString.codeUnitAt(0)); - }); - return string.length - remain.toString().length; - } - - /// Returns the index into the string of the previous character boundary - /// before the given index. - /// - /// The character boundary is determined by the characters package, so - /// surrogate pairs and extended grapheme clusters are considered. - /// - /// The index must be between 0 and string.length, inclusive. If index is 0, - /// 0 will be returned. - /// - /// Setting includeWhitespace to false will only return the index of non-space - /// characters. - int _previousCharacter(int index, String string, includeWhitespace) { - assert(index >= 0 && index <= string.length); - if (index == 0) { - return 0; - } - - var count = 0; - int? lastNonWhitespace; - for (final currentString in string.characters) { - if (!includeWhitespace && - !WHITE_SPACE.contains( - currentString.characters.first.toString().codeUnitAt(0))) { - lastNonWhitespace = count; - } - if (count + currentString.length >= index) { - return includeWhitespace ? count : lastNonWhitespace ?? 0; - } - count += currentString.length; - } - return 0; - } - - TextSelection _handleMovingCursorVertically( - bool upKey, - bool downKey, - bool shift, - TextSelection selection, - TextSelection newSelection, - String plainText) { - final originPosition = TextPosition( - offset: upKey ? selection.baseOffset : selection.extentOffset); - - final child = getRenderEditor()!.childAtPosition(originPosition); - final localPosition = TextPosition( - offset: originPosition.offset - child.getContainer().documentOffset); - - var position = upKey - ? child.getPositionAbove(localPosition) - : child.getPositionBelow(localPosition); - - if (position == null) { - final sibling = upKey - ? getRenderEditor()!.childBefore(child) - : getRenderEditor()!.childAfter(child); - if (sibling == null) { - position = TextPosition(offset: upKey ? 0 : plainText.length - 1); - } else { - final finalOffset = Offset( - child.getOffsetForCaret(localPosition).dx, - sibling - .getOffsetForCaret(TextPosition( - offset: upKey ? sibling.getContainer().length - 1 : 0)) - .dy); - final siblingPosition = sibling.getPositionForOffset(finalOffset); - position = TextPosition( - offset: - sibling.getContainer().documentOffset + siblingPosition.offset); - } - } else { - position = TextPosition( - offset: child.getContainer().documentOffset + position.offset); - } - - if (position.offset == newSelection.extentOffset) { - if (downKey) { - newSelection = newSelection.copyWith(extentOffset: plainText.length); - } else if (upKey) { - newSelection = newSelection.copyWith(extentOffset: 0); - } - _wasSelectingVerticallyWithKeyboard = shift; - return newSelection; - } - - if (_wasSelectingVerticallyWithKeyboard && shift) { - newSelection = newSelection.copyWith(extentOffset: _cursorResetLocation); - _wasSelectingVerticallyWithKeyboard = false; - return newSelection; - } - newSelection = newSelection.copyWith(extentOffset: position.offset); - _cursorResetLocation = newSelection.extentOffset; - return newSelection; - } - - TextSelection _placeCollapsedSelection(TextSelection selection, - TextSelection newSelection, bool leftKey, bool rightKey) { - var newOffset = newSelection.extentOffset; - if (!selection.isCollapsed) { - if (leftKey) { - newOffset = newSelection.baseOffset < newSelection.extentOffset - ? newSelection.baseOffset - : newSelection.extentOffset; - } else if (rightKey) { - newOffset = newSelection.baseOffset > newSelection.extentOffset - ? newSelection.baseOffset - : newSelection.extentOffset; - } - } - return TextSelection.fromPosition(TextPosition(offset: newOffset)); - } -} diff --git a/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart index 0ee0ed66..36a5b5b2 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart @@ -1,9 +1,8 @@ import 'dart:math' as math; -import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import '../../../utils/diff_delta.dart'; import '../editor.dart'; @@ -11,13 +10,17 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState implements TextSelectionDelegate { @override TextEditingValue get textEditingValue { - return getTextEditingValue(); + return widget.controller.plainTextEditingValue; } @override set textEditingValue(TextEditingValue value) { - // deprecated - setTextEditingValue(value, SelectionChangedCause.keyboard); + final cursorPosition = value.selection.extentOffset; + final oldText = widget.controller.document.toPlainText(); + final newText = value.text; + final diff = getDiff(oldText, newText, cursorPosition); + widget.controller.replaceText( + diff.start, diff.deleted.length, diff.inserted, value.selection); } @override @@ -85,7 +88,7 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState @override void userUpdateTextEditingValue( TextEditingValue value, SelectionChangedCause cause) { - setTextEditingValue(value, cause); + textEditingValue = value; } @override @@ -99,119 +102,4 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState @override bool get selectAllEnabled => widget.toolbarOptions.selectAll; - - void setSelection(TextSelection nextSelection, SelectionChangedCause cause) { - if (nextSelection == textEditingValue.selection) { - return; - } - setTextEditingValue( - textEditingValue.copyWith(selection: nextSelection), - cause, - ); - } - - @override - void copySelection(SelectionChangedCause cause) { - final selection = textEditingValue.selection; - if (selection.isCollapsed || !selection.isValid) { - return; - } - Clipboard.setData( - ClipboardData(text: selection.textInside(textEditingValue.text))); - - if (cause == SelectionChangedCause.toolbar) { - bringIntoView(textEditingValue.selection.extent); - hideToolbar(false); - - switch (defaultTargetPlatform) { - case TargetPlatform.iOS: - break; - case TargetPlatform.macOS: - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.windows: - // Collapse the selection and hide the toolbar and handles. - userUpdateTextEditingValue( - TextEditingValue( - text: textEditingValue.text, - selection: TextSelection.collapsed( - offset: textEditingValue.selection.end), - ), - SelectionChangedCause.toolbar, - ); - break; - } - } - } - - @override - void cutSelection(SelectionChangedCause cause) { - final selection = textEditingValue.selection; - if (readOnly || !selection.isValid || selection.isCollapsed) { - return; - } - final text = textEditingValue.text; - Clipboard.setData(ClipboardData(text: selection.textInside(text))); - setTextEditingValue( - TextEditingValue( - text: selection.textBefore(text) + selection.textAfter(text), - selection: TextSelection.collapsed( - offset: math.min(selection.start, selection.end), - affinity: selection.affinity, - ), - ), - cause, - ); - - if (cause == SelectionChangedCause.toolbar) { - bringIntoView(textEditingValue.selection.extent); - hideToolbar(); - } - } - - @override - Future pasteText(SelectionChangedCause cause) async { - final selection = textEditingValue.selection; - if (readOnly || !selection.isValid) { - return; - } - final text = textEditingValue.text; - // See https://github.com/flutter/flutter/issues/11427 - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data == null) { - return; - } - setTextEditingValue( - TextEditingValue( - text: - selection.textBefore(text) + data.text! + selection.textAfter(text), - selection: TextSelection.collapsed( - offset: math.min(selection.start, selection.end) + data.text!.length, - affinity: selection.affinity, - ), - ), - cause, - ); - - if (cause == SelectionChangedCause.toolbar) { - bringIntoView(textEditingValue.selection.extent); - hideToolbar(); - } - } - - @override - void selectAll(SelectionChangedCause cause) { - setSelection( - textEditingValue.selection.copyWith( - baseOffset: 0, - extentOffset: textEditingValue.text.length, - ), - cause, - ); - - if (cause == SelectionChangedCause.toolbar) { - bringIntoView(textEditingValue.selection.extent); - } - } } diff --git a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart index cfb13e62..c0443349 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart @@ -49,7 +49,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState } if (!hasConnection) { - _lastKnownRemoteTextEditingValue = getTextEditingValue(); + _lastKnownRemoteTextEditingValue = textEditingValue; _textInputConnection = TextInput.attach( this, TextInputConfiguration( @@ -90,12 +90,14 @@ mixin RawEditorStateTextInputClientMixin on EditorState return; } + final value = textEditingValue; + // Since we don't keep track of the composing range in value provided // by the Controller we need to add it here manually before comparing // with the last known remote value. // It is important to prevent excessive remote updates as it can cause // race conditions. - final actualValue = getTextEditingValue().copyWith( + final actualValue = value.copyWith( composing: _lastKnownRemoteTextEditingValue!.composing, ); @@ -103,8 +105,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState return; } - final shouldRemember = - getTextEditingValue().text != _lastKnownRemoteTextEditingValue!.text; + final shouldRemember = value.text != _lastKnownRemoteTextEditingValue!.text; _lastKnownRemoteTextEditingValue = actualValue; _textInputConnection!.setEditingState( // Set composing to (-1, -1), otherwise an exception will be thrown if diff --git a/lib/widgets/keyboard_listener.dart b/lib/widgets/keyboard_listener.dart deleted file mode 100644 index 20c72b74..00000000 --- a/lib/widgets/keyboard_listener.dart +++ /dev/null @@ -1,3 +0,0 @@ -/// TODO: Remove this file in the next breaking release, because implementation -/// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../src/widgets/keyboard_listener.dart'; diff --git a/lib/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart b/lib/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart deleted file mode 100644 index ab326cec..00000000 --- a/lib/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart +++ /dev/null @@ -1,3 +0,0 @@ -/// TODO: Remove this file in the next breaking release, because implementation -/// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart';