Upgrade to 2.8 (#519)

pull/526/head
X Code 3 years ago committed by GitHub
parent 49040447b1
commit 7ee58bf46d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 29
      lib/src/utils/diff_delta.dart
  2. 157
      lib/src/widgets/editor.dart
  3. 129
      lib/src/widgets/keyboard_listener.dart
  4. 105
      lib/src/widgets/raw_editor.dart
  5. 368
      lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart
  6. 130
      lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart
  7. 9
      lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart
  8. 3
      lib/widgets/keyboard_listener.dart
  9. 3
      lib/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart

@ -2,35 +2,6 @@ import 'dart:math' as math;
import '../models/quill_delta.dart';
const Set<int> 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);

@ -47,13 +47,10 @@ const linkPrefixes = [
'http'
];
abstract class EditorState extends State<RawEditor> {
abstract class EditorState extends State<RawEditor>
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<RawEditor> {
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

@ -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<LogicalKeyboardKey> _moveKeys = <LogicalKeyboardKey>{
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.arrowUp,
LogicalKeyboardKey.arrowDown,
};
static final Set<LogicalKeyboardKey> _shortcutKeys = <LogicalKeyboardKey>{
LogicalKeyboardKey.keyA,
LogicalKeyboardKey.keyC,
LogicalKeyboardKey.keyV,
LogicalKeyboardKey.keyX,
LogicalKeyboardKey.keyZ.toUpperCase(),
LogicalKeyboardKey.keyZ,
LogicalKeyboardKey.delete,
LogicalKeyboardKey.backspace,
};
static final Set<LogicalKeyboardKey> _nonModifierKeys = <LogicalKeyboardKey>{
..._shortcutKeys,
..._moveKeys,
};
static final Set<LogicalKeyboardKey> _modifierKeys = <LogicalKeyboardKey>{
LogicalKeyboardKey.shift,
LogicalKeyboardKey.control,
LogicalKeyboardKey.alt,
};
static final Set<LogicalKeyboardKey> _macOsModifierKeys =
<LogicalKeyboardKey>{
LogicalKeyboardKey.shift,
LogicalKeyboardKey.meta,
LogicalKeyboardKey.alt,
};
static final Set<LogicalKeyboardKey> _interestingKeys = <LogicalKeyboardKey>{
..._modifierKeys,
..._macOsModifierKeys,
..._nonModifierKeys,
};
static final Map<LogicalKeyboardKey, InputShortcut> _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;
}
}

@ -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<RawEditor>,
WidgetsBindingObserver,
TickerProviderStateMixin<RawEditor>,
RawEditorStateKeyboardMixin,
TextEditingActionTarget,
RawEditorStateTextInputClientMixin,
RawEditorStateSelectionDelegateMixin {
final GlobalKey _editorKey = GlobalKey();
// Keyboard
late KeyboardEventHandler _keyboardListener;
KeyboardVisibilityController? _keyboardVisibilityController;
StreamSubscription<bool>? _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<void> 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;

@ -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<void> 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));
}
}

@ -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<void> 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);
}
}
}

@ -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

@ -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';

@ -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';
Loading…
Cancel
Save