import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_quill/models/documents/document.dart'; import 'package:flutter_quill/utils/diff_delta.dart'; import 'package:flutter_quill/widgets/text_selection.dart'; import 'controller.dart'; import 'cursor.dart'; import 'delegate.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 }; abstract class RenderAbstractEditor { TextSelection selectWordAtPosition(TextPosition position); TextSelection selectLineAtPosition(TextPosition position); double preferredLineHeight(TextPosition position); TextPosition getPositionForOffset(Offset offset); List getEndpointsForSelection( TextSelection textSelection); void handleTapDown(TapDownDetails details); void selectWordsInRange( Offset from, Offset to, SelectionChangedCause cause, ); void selectWordEdge(SelectionChangedCause cause); void selectPositionAt(Offset from, Offset to, SelectionChangedCause cause); void selectWord(SelectionChangedCause cause); void selectPosition(SelectionChangedCause cause); } class QuillEditor extends StatefulWidget { final QuillController controller; final FocusNode focusNode; final ScrollController scrollController; final bool scrollable; final EdgeInsetsGeometry padding; final bool autoFocus; final bool showCursor; final bool readOnly; final bool enableInteractiveSelection; final double minHeight; final double maxHeight; final bool expands; final TextCapitalization textCapitalization; final Brightness keyboardAppearance; final ScrollPhysics scrollPhysics; final ValueChanged onLaunchUrl; final EmbedBuilder embedBuilder; QuillEditor( this.controller, this.focusNode, this.scrollController, this.scrollable, this.padding, this.autoFocus, this.showCursor, this.readOnly, this.enableInteractiveSelection, this.minHeight, this.maxHeight, this.expands, this.textCapitalization, this.keyboardAppearance, this.scrollPhysics, this.onLaunchUrl, this.embedBuilder) : assert(controller != null), assert(scrollController != null), assert(scrollable != null), assert(autoFocus != null), assert(readOnly != null), assert(embedBuilder != null); @override _QuillEditorState createState() => _QuillEditorState(); } class _QuillEditorState extends State implements EditorTextSelectionGestureDetectorBuilderDelegate { final GlobalKey _editorKey = GlobalKey(); EditorTextSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder; @override void initState() { super.initState(); _selectionGestureDetectorBuilder = _QuillEditorSelectionGestureDetectorBuilder(this); } @override Widget build(BuildContext context) { ThemeData theme = Theme.of(context); TextSelectionThemeData selectionTheme = TextSelectionTheme.of(context); TextSelectionControls textSelectionControls; bool paintCursorAboveText; bool cursorOpacityAnimates; Offset cursorOffset; Color cursorColor; Color selectionColor; Radius cursorRadius; switch (theme.platform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: textSelectionControls = materialTextSelectionControls; paintCursorAboveText = false; cursorOpacityAnimates = false; cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary; selectionColor = selectionTheme.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); break; case TargetPlatform.iOS: case TargetPlatform.macOS: CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); textSelectionControls = cupertinoTextSelectionControls; paintCursorAboveText = true; cursorOpacityAnimates = true; cursorColor ??= selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; selectionColor = selectionTheme.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40); cursorRadius ??= const Radius.circular(2.0); cursorOffset = Offset( iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); break; default: throw UnimplementedError(); } return _selectionGestureDetectorBuilder.build( HitTestBehavior.translucent, RawEditor( _editorKey, widget.controller, widget.focusNode, widget.scrollController, widget.scrollable, widget.padding, widget.readOnly, widget.onLaunchUrl, ToolbarOptions( copy: true, cut: true, paste: true, selectAll: true, ), theme.platform == TargetPlatform.iOS || theme.platform == TargetPlatform.android, widget.showCursor, CursorStyle( color: cursorColor, backgroundColor: Colors.grey, width: 2.0, radius: cursorRadius, offset: cursorOffset, paintAboveText: paintCursorAboveText, opacityAnimates: cursorOpacityAnimates, ), widget.textCapitalization, widget.maxHeight, widget.minHeight, widget.expands, widget.autoFocus, selectionColor, textSelectionControls, widget.keyboardAppearance, widget.enableInteractiveSelection, widget.scrollPhysics, widget.embedBuilder), ); } @override GlobalKey getEditableTextKey() { return _editorKey; } @override bool getForcePressEnabled() { return false; } @override bool getSelectionEnabled() { return widget.enableInteractiveSelection; } } class _QuillEditorSelectionGestureDetectorBuilder extends EditorTextSelectionGestureDetectorBuilder { final _QuillEditorState _state; _QuillEditorSelectionGestureDetectorBuilder(this._state) : super(_state); @override onForcePressStart(ForcePressDetails details) { super.onForcePressStart(details); } } class RawEditor extends StatefulWidget { final QuillController controller; final FocusNode focusNode; final ScrollController scrollController; final bool scrollable; final EdgeInsetsGeometry padding; final bool readOnly; final ValueChanged onLaunchUrl; final ToolbarOptions toolbarOptions; final bool showSelectionHandles; final bool showCursor; final CursorStyle cursorStyle; final TextCapitalization textCapitalization; final double maxHeight; final double minHeight; final bool expands; final bool autoFocus; final Color selectionColor; final TextSelectionControls selectionCtrls; final Brightness keyboardAppearance; final bool enableInteractiveSelection; final ScrollPhysics scrollPhysics; final EmbedBuilder embedBuilder; RawEditor( Key key, this.controller, this.focusNode, this.scrollController, this.scrollable, this.padding, this.readOnly, this.onLaunchUrl, this.toolbarOptions, this.showSelectionHandles, bool showCursor, this.cursorStyle, this.textCapitalization, this.maxHeight, this.minHeight, this.expands, this.autoFocus, this.selectionColor, this.selectionCtrls, this.keyboardAppearance, this.enableInteractiveSelection, this.scrollPhysics, this.embedBuilder) : assert(controller != null), assert(focusNode != null), assert(scrollable || scrollController != null), assert(selectionColor != null), assert(enableInteractiveSelection != null), assert(showSelectionHandles != null), assert(readOnly != null), assert(maxHeight == null || maxHeight > 0), assert(minHeight == null || minHeight >= 0), assert( maxHeight == null || minHeight == null || maxHeight >= minHeight), assert(autoFocus != null), assert(toolbarOptions != null), showCursor = showCursor ?? !readOnly, assert(embedBuilder != null), super(key: key); @override State createState() { return RawEditorState(); } } class RawEditorState extends EditorState with AutomaticKeepAliveClientMixin, WidgetsBindingObserver, TickerProviderStateMixin implements TextSelectionDelegate, TextInputClient { final GlobalKey _editorKey = GlobalKey(); final List _sentRemoteValues = []; TextInputConnection _textInputConnection; TextEditingValue _lastKnownRemoteTextEditingValue; int _cursorResetLocation = -1; bool _wasSelectingVerticallyWithKeyboard = false; handleCursorMovement( LogicalKeyboardKey key, bool wordModifier, bool lineModifier, bool shift, ) { if (wordModifier && lineModifier) { return; } TextSelection selection = widget.controller.selection; assert(selection != null); TextSelection newSelection = widget.controller.selection; String plainText = textEditingValue.text; bool 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) {} if (!shift) {} widget.controller.updateSelection(newSelection, ChangeSource.LOCAL); } TextSelection _jumpToBeginOrEndOfWord( TextSelection newSelection, bool wordModifier, bool leftKey, bool rightKey, String plainText, bool lineModifier, bool shift) { if (wordModifier) { if (leftKey) { TextSelection textSelection = getRenderEditor().selectWordAtPosition( TextPosition( offset: _previousCharacter( newSelection.extentOffset, plainText, false))); return newSelection.copyWith(extentOffset: textSelection.baseOffset); } TextSelection textSelection = getRenderEditor().selectWordAtPosition( TextPosition( offset: _nextCharacter(newSelection.extentOffset, plainText, false))); return newSelection.copyWith(extentOffset: textSelection.extentOffset); } else if (lineModifier) { if (leftKey) { TextSelection textSelection = getRenderEditor().selectLineAtPosition( TextPosition( offset: _previousCharacter( newSelection.extentOffset, plainText, false))); return newSelection.copyWith(extentOffset: textSelection.baseOffset); } int startPoint = newSelection.extentOffset; if (startPoint < plainText.length) { TextSelection textSelection = getRenderEditor() .selectLineAtPosition(TextPosition(offset: startPoint)); return newSelection.copyWith(extentOffset: textSelection.extentOffset); } return newSelection; } if (rightKey && newSelection.extentOffset < plainText.length) { int nextExtent = _nextCharacter(newSelection.extentOffset, plainText, true); int distance = nextExtent - newSelection.extentOffset; newSelection = newSelection.copyWith(extentOffset: nextExtent); if (shift) { _cursorResetLocation += distance; } return newSelection; } if (leftKey && newSelection.extentOffset > 0) { int previousExtent = _previousCharacter(newSelection.extentOffset, plainText, true); int distance = newSelection.extentOffset - previousExtent; newSelection = newSelection.copyWith(extentOffset: previousExtent); if (shift) { _cursorResetLocation -= distance; } return newSelection; } return newSelection; } int _nextCharacter(int index, String string, bool includeWhitespace) { assert(index >= 0 && index <= string.length); if (index == string.length) { return string.length; } int count = 0; Characters remain = string.characters.skipWhile((String 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; } int _previousCharacter(int index, String string, includeWhitespace) { assert(index >= 0 && index <= string.length); if (index == 0) { return 0; } int count = 0; int lastNonWhitespace; for (String 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; } bool get hasConnection => _textInputConnection != null && _textInputConnection.attached; openConnectionIfNeeded() { if (widget.readOnly) { return; } if (!hasConnection) { _lastKnownRemoteTextEditingValue = textEditingValue; _textInputConnection = TextInput.attach( this, TextInputConfiguration( inputType: TextInputType.multiline, readOnly: widget.readOnly, obscureText: false, autocorrect: false, inputAction: TextInputAction.newline, keyboardAppearance: widget.keyboardAppearance, textCapitalization: widget.textCapitalization, ), ); _textInputConnection.setEditingState(_lastKnownRemoteTextEditingValue); _sentRemoteValues.add(_lastKnownRemoteTextEditingValue); } _textInputConnection.show(); } closeConnectionIfNeeded() { if (!hasConnection) { return; } _textInputConnection.close(); _textInputConnection = null; _lastKnownRemoteTextEditingValue = null; _sentRemoteValues.clear(); } updateRemoteValueIfNeeded() { if (!hasConnection) { return; } TextEditingValue actualValue = textEditingValue.copyWith( composing: _lastKnownRemoteTextEditingValue.composing, ); if (actualValue == _lastKnownRemoteTextEditingValue) { return; } bool shouldRemember = textEditingValue.text != _lastKnownRemoteTextEditingValue.text; _lastKnownRemoteTextEditingValue = actualValue; _textInputConnection.setEditingState(actualValue); if (shouldRemember) { _sentRemoteValues.add(actualValue); } } @override TextEditingValue get currentTextEditingValue => _lastKnownRemoteTextEditingValue; @override AutofillScope get currentAutofillScope => null; @override void updateEditingValue(TextEditingValue value) { if (widget.readOnly) { return; } if (_sentRemoteValues.contains(value)) { _sentRemoteValues.remove(value); return; } if (_lastKnownRemoteTextEditingValue == value) { return; } if (_lastKnownRemoteTextEditingValue.text == value.text && _lastKnownRemoteTextEditingValue.selection == value.selection) { _lastKnownRemoteTextEditingValue = value; return; } TextEditingValue effectiveLastKnownValue = _lastKnownRemoteTextEditingValue; _lastKnownRemoteTextEditingValue = value; String oldText = effectiveLastKnownValue.text; String text = value.text; int cursorPosition = value.selection.extentOffset; Diff diff = getDiff(oldText, text, cursorPosition); widget.controller.replaceText( diff.start, diff.deleted.length, diff.inserted, value.selection); } @override TextEditingValue get textEditingValue { return widget.controller.plainTextEditingValue; } @override set textEditingValue(TextEditingValue value) { widget.controller.updateSelection(value.selection, ChangeSource.LOCAL); } @override void performAction(TextInputAction action) {} @override void performPrivateCommand(String action, Map data) {} @override void updateFloatingCursor(RawFloatingCursorPoint point) { throw UnimplementedError(); } @override void showAutocorrectionPromptRect(int start, int end) { throw UnimplementedError(); } @override void bringIntoView(TextPosition position) { // TODO: implement bringIntoView } @override void connectionClosed() { if (!hasConnection) { return; } _textInputConnection.connectionClosedReceived(); _textInputConnection = null; _lastKnownRemoteTextEditingValue = null; _sentRemoteValues.clear(); } @override Widget build(BuildContext context) { // TODO: implement build throw UnimplementedError(); } @override RenderEditor getRenderEditor() { // TODO: implement getRenderEditor throw UnimplementedError(); } @override EditorTextSelectionOverlay getSelectionOverlay() { // TODO: implement getSelectionOverlay throw UnimplementedError(); } @override TextEditingValue getTextEditingValue() { // TODO: implement getTextEditingValue throw UnimplementedError(); } @override void hideToolbar() { if (getSelectionOverlay()?.toolbar != null) { getSelectionOverlay()?.hideToolbar(); } } @override bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly; @override bool get copyEnabled => widget.toolbarOptions.copy; @override bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly; @override bool get selectAllEnabled => widget.toolbarOptions.selectAll; @override void requestKeyboard() { // TODO: implement requestKeyboard } @override void setTextEditingValue(TextEditingValue value) { // TODO: implement setTextEditingValue } @override bool showToolbar() { // TODO: implement showToolbar throw UnimplementedError(); } @override // TODO: implement wantKeepAlive bool get wantKeepAlive => throw UnimplementedError(); } class RenderEditor extends RenderEditableContainerBox implements RenderAbstractEditor { Document document; TextSelection selection; bool _hasFocus = false; setDocument(Document doc) { assert(doc != null); if (document == doc) { return; } document = doc; markNeedsLayout(); } setHasFocus(bool h) { assert(h != null); if (_hasFocus == h) { return; } _hasFocus = h; markNeedsSemanticsUpdate(); } setSelection(TextSelection t) { if (selection == t) { return; } selection = t; markNeedsPaint(); } @override List getEndpointsForSelection( TextSelection textSelection) { // TODO: implement getEndpointsForSelection throw UnimplementedError(); } @override TextPosition getPositionForOffset(Offset offset) { // TODO: implement getPositionForOffset throw UnimplementedError(); } @override void handleTapDown(TapDownDetails details) { // TODO: implement handleTapDown } @override double preferredLineHeight(TextPosition position) { // TODO: implement preferredLineHeight throw UnimplementedError(); } @override TextSelection selectLineAtPosition(TextPosition position) { // TODO: implement selectLineAtPosition throw UnimplementedError(); } @override void selectPosition(SelectionChangedCause cause) { // TODO: implement selectPosition } @override void selectPositionAt(Offset from, Offset to, SelectionChangedCause cause) { // TODO: implement selectPositionAt } @override void selectWord(SelectionChangedCause cause) { // TODO: implement selectWord } @override TextSelection selectWordAtPosition(TextPosition position) { // TODO: implement selectWordAtPosition throw UnimplementedError(); } @override void selectWordEdge(SelectionChangedCause cause) { // TODO: implement selectWordEdge } @override void selectWordsInRange(Offset from, Offset to, SelectionChangedCause cause) { assert(cause != null && from != null); // TODO: implement selectWordsInRange } } class RenderEditableContainerBox extends RenderBox { Container _container; TextDirection _textDirection; setContainer(Container c) { assert(c != null); if (_container == c) { return; } _container = c; markNeedsLayout(); } setTextDirection(TextDirection t) { if (_textDirection == t) { return; } _textDirection = t; } } abstract class EditorState extends State { TextEditingValue getTextEditingValue(); void setTextEditingValue(TextEditingValue value); RenderEditor getRenderEditor(); EditorTextSelectionOverlay getSelectionOverlay(); bool showToolbar(); void hideToolbar(); void requestKeyboard(); }