dartlangeditorflutterflutter-appsflutter-examplesflutter-packageflutter-widgetquillquill-deltaquilljsreactquillrich-textrich-text-editorwysiwygwysiwyg-editor
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1070 lines
33 KiB
1070 lines
33 KiB
import 'package:flutter/cupertino.dart'; |
|
import 'package:flutter/foundation.dart'; |
|
import 'package:flutter/gestures.dart'; |
|
import 'package:flutter/material.dart'; |
|
import 'package:flutter/rendering.dart'; |
|
import 'package:flutter/scheduler.dart'; |
|
import 'package:flutter/services.dart'; |
|
import 'package:flutter_quill/models/documents/attribute.dart'; |
|
import 'package:flutter_quill/models/documents/document.dart'; |
|
import 'package:flutter_quill/models/documents/nodes/block.dart'; |
|
import 'package:flutter_quill/models/documents/nodes/line.dart'; |
|
import 'package:flutter_quill/models/documents/nodes/node.dart'; |
|
import 'package:flutter_quill/utils/diff_delta.dart'; |
|
import 'package:flutter_quill/widgets/default_styles.dart'; |
|
import 'package:flutter_quill/widgets/proxy.dart'; |
|
import 'package:flutter_quill/widgets/text_block.dart'; |
|
import 'package:flutter_quill/widgets/text_line.dart'; |
|
import 'package:flutter_quill/widgets/text_selection.dart'; |
|
import 'package:tuple/tuple.dart'; |
|
|
|
import 'box.dart'; |
|
import 'controller.dart'; |
|
import 'cursor.dart'; |
|
import 'delegate.dart'; |
|
import 'editor.dart'; |
|
import 'keyboard_listener.dart'; |
|
|
|
class RawEditor extends StatefulWidget { |
|
final QuillController controller; |
|
final FocusNode focusNode; |
|
final ScrollController scrollController; |
|
final bool scrollable; |
|
final EdgeInsetsGeometry padding; |
|
final bool readOnly; |
|
final ValueChanged<String> 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, 'controller cannot be null'), |
|
assert(focusNode != null, 'focusNode cannot be null'), |
|
assert(scrollable || scrollController != null, |
|
'scrollController cannot be null'), |
|
assert(selectionColor != null, 'selectionColor cannot be null'), |
|
assert(enableInteractiveSelection != null, |
|
'enableInteractiveSelection cannot be null'), |
|
assert(showSelectionHandles != null, |
|
'showSelectionHandles cannot be null'), |
|
assert(readOnly != null, 'readOnly cannot be null'), |
|
assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'), |
|
assert(minHeight == null || minHeight >= 0, 'minHeight cannot be null'), |
|
assert(maxHeight == null || minHeight == null || maxHeight >= minHeight, |
|
'maxHeight cannot be null'), |
|
assert(autoFocus != null, 'autoFocus cannot be null'), |
|
assert(toolbarOptions != null, 'toolbarOptions cannot be null'), |
|
showCursor = showCursor ?? !readOnly, |
|
assert(embedBuilder != null, 'embedBuilder cannot be null'), |
|
assert(expands != null, 'expands cannot be null'), |
|
assert(padding != null), |
|
super(key: key); |
|
|
|
@override |
|
State<StatefulWidget> createState() { |
|
return RawEditorState(); |
|
} |
|
} |
|
|
|
class RawEditorState extends EditorState |
|
with |
|
AutomaticKeepAliveClientMixin<RawEditor>, |
|
WidgetsBindingObserver, |
|
TickerProviderStateMixin<RawEditor> |
|
implements TextSelectionDelegate, TextInputClient { |
|
final GlobalKey _editorKey = GlobalKey(); |
|
final List<TextEditingValue> _sentRemoteValues = []; |
|
TextInputConnection _textInputConnection; |
|
TextEditingValue _lastKnownRemoteTextEditingValue; |
|
int _cursorResetLocation = -1; |
|
bool _wasSelectingVerticallyWithKeyboard = false; |
|
EditorTextSelectionOverlay _selectionOverlay; |
|
FocusAttachment _focusAttachment; |
|
CursorCont _cursorCont; |
|
ScrollController _scrollController; |
|
KeyboardListener _keyboardListener; |
|
bool _didAutoFocus = false; |
|
DefaultStyles _styles; |
|
final ClipboardStatusNotifier _clipboardStatus = ClipboardStatusNotifier(); |
|
final LayerLink _toolbarLayerLink = LayerLink(); |
|
final LayerLink _startHandleLayerLink = LayerLink(); |
|
final LayerLink _endHandleLayerLink = LayerLink(); |
|
|
|
bool get _hasFocus => widget.focusNode.hasFocus; |
|
|
|
TextDirection get _textDirection { |
|
TextDirection result = Directionality.of(context); |
|
assert(result != null); |
|
return result; |
|
} |
|
|
|
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) { |
|
newSelection = _handleMovingCursorVertically( |
|
upKey, downKey, shift, selection, newSelection, plainText); |
|
} |
|
|
|
if (!shift) { |
|
newSelection = |
|
_placeCollapsedSelection(selection, newSelection, leftKey, rightKey); |
|
} |
|
|
|
widget.controller.updateSelection(newSelection, ChangeSource.LOCAL); |
|
} |
|
|
|
TextSelection _placeCollapsedSelection(TextSelection selection, |
|
TextSelection newSelection, bool leftKey, bool rightKey) { |
|
int 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)); |
|
} |
|
|
|
TextSelection _handleMovingCursorVertically( |
|
bool upKey, |
|
bool downKey, |
|
bool shift, |
|
TextSelection selection, |
|
TextSelection newSelection, |
|
String plainText) { |
|
TextPosition originPosition = TextPosition( |
|
offset: upKey ? selection.baseOffset : selection.extentOffset); |
|
|
|
RenderEditableBox child = getRenderEditor().childAtPosition(originPosition); |
|
TextPosition localPosition = TextPosition( |
|
offset: |
|
originPosition.offset - child.getContainer().getDocumentOffset()); |
|
|
|
TextPosition position = upKey |
|
? child.getPositionAbove(localPosition) |
|
: child.getPositionBelow(localPosition); |
|
|
|
if (position == null) { |
|
var sibling = upKey |
|
? getRenderEditor().childBefore(child) |
|
: getRenderEditor().childAfter(child); |
|
if (sibling == null) { |
|
position = TextPosition(offset: upKey ? 0 : plainText.length - 1); |
|
} else { |
|
Offset finalOffset = Offset( |
|
child.getOffsetForCaret(localPosition).dx, |
|
sibling |
|
.getOffsetForCaret(TextPosition( |
|
offset: upKey ? sibling.getContainer().length - 1 : 0)) |
|
.dy); |
|
TextPosition siblingPosition = |
|
sibling.getPositionForOffset(finalOffset); |
|
position = TextPosition( |
|
offset: sibling.getContainer().getDocumentOffset() + |
|
siblingPosition.offset); |
|
} |
|
} else { |
|
position = TextPosition( |
|
offset: child.getContainer().getDocumentOffset() + 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 _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<String, dynamic> data) {} |
|
|
|
@override |
|
void updateFloatingCursor(RawFloatingCursorPoint point) { |
|
throw UnimplementedError(); |
|
} |
|
|
|
@override |
|
void showAutocorrectionPromptRect(int start, int end) { |
|
throw UnimplementedError(); |
|
} |
|
|
|
@override |
|
void bringIntoView(TextPosition position) {} |
|
|
|
@override |
|
void connectionClosed() { |
|
if (!hasConnection) { |
|
return; |
|
} |
|
_textInputConnection.connectionClosedReceived(); |
|
_textInputConnection = null; |
|
_lastKnownRemoteTextEditingValue = null; |
|
_sentRemoteValues.clear(); |
|
} |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
assert(debugCheckHasMediaQuery(context)); |
|
_focusAttachment.reparent(); |
|
super.build(context); |
|
|
|
Widget child = CompositedTransformTarget( |
|
link: _toolbarLayerLink, |
|
child: Semantics( |
|
child: _Editor( |
|
key: _editorKey, |
|
children: _buildChildren(context), |
|
document: widget.controller.document, |
|
selection: widget.controller.selection, |
|
hasFocus: _hasFocus, |
|
textDirection: _textDirection, |
|
startHandleLayerLink: _startHandleLayerLink, |
|
endHandleLayerLink: _endHandleLayerLink, |
|
onSelectionChanged: _handleSelectionChanged, |
|
padding: widget.padding, |
|
), |
|
), |
|
); |
|
|
|
if (widget.scrollable) { |
|
EdgeInsets baselinePadding = |
|
EdgeInsets.only(top: _styles.paragraph.verticalSpacing.item1); |
|
child = BaselineProxy( |
|
textStyle: _styles.paragraph.style, |
|
padding: baselinePadding, |
|
child: SingleChildScrollView( |
|
controller: _scrollController, |
|
physics: widget.scrollPhysics, |
|
child: child, |
|
), |
|
); |
|
} |
|
|
|
BoxConstraints constraints = widget.expands |
|
? BoxConstraints.expand() |
|
: BoxConstraints( |
|
minHeight: widget.minHeight ?? 0.0, |
|
maxHeight: widget.maxHeight ?? double.infinity); |
|
|
|
return QuillStyles( |
|
data: _styles, |
|
child: MouseRegion( |
|
cursor: SystemMouseCursors.text, |
|
child: Container( |
|
constraints: constraints, |
|
child: child, |
|
), |
|
), |
|
); |
|
} |
|
|
|
_handleSelectionChanged( |
|
TextSelection selection, SelectionChangedCause cause) { |
|
widget.controller.updateSelection(selection, ChangeSource.LOCAL); |
|
|
|
_selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); |
|
|
|
requestKeyboard(); |
|
} |
|
|
|
_buildChildren(BuildContext context) { |
|
final result = <Widget>[]; |
|
for (Node node in widget.controller.document.root.children) { |
|
if (node is Line) { |
|
TextLine textLine = TextLine( |
|
line: node, |
|
textDirection: _textDirection, |
|
embedBuilder: widget.embedBuilder, |
|
); |
|
EditableTextLine editableTextLine = EditableTextLine( |
|
node, |
|
null, |
|
textLine, |
|
0, |
|
_getVerticalSpacingForLine(node, _styles), |
|
_textDirection, |
|
widget.controller.selection, |
|
widget.selectionColor, |
|
widget.enableInteractiveSelection, |
|
_hasFocus, |
|
MediaQuery.of(context).devicePixelRatio, |
|
_cursorCont); |
|
result.add(editableTextLine); |
|
} else if (node is Block) { |
|
Map<String, Attribute> attrs = node.style.attributes; |
|
EditableTextBlock editableTextBlock = EditableTextBlock( |
|
node, |
|
_textDirection, |
|
_getVerticalSpacingForBlock(node, _styles), |
|
widget.controller.selection, |
|
widget.selectionColor, |
|
widget.enableInteractiveSelection, |
|
_hasFocus, |
|
attrs.containsKey(Attribute.codeBlock.key) |
|
? EdgeInsets.all(16.0) |
|
: null, |
|
widget.embedBuilder, |
|
_cursorCont); |
|
result.add(editableTextBlock); |
|
} else { |
|
throw StateError('Unreachable.'); |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
Tuple2<double, double> _getVerticalSpacingForLine( |
|
Line line, DefaultStyles defaultStyles) { |
|
Map<String, Attribute> attrs = line.style.attributes; |
|
if (attrs.containsKey(Attribute.header.key)) { |
|
int level = attrs[Attribute.header.key].value; |
|
switch (level) { |
|
case 1: |
|
return defaultStyles.h1.verticalSpacing; |
|
case 2: |
|
return defaultStyles.h2.verticalSpacing; |
|
case 3: |
|
return defaultStyles.h3.verticalSpacing; |
|
default: |
|
throw ('Invalid level $level'); |
|
} |
|
} |
|
|
|
return defaultStyles.paragraph.verticalSpacing; |
|
} |
|
|
|
Tuple2<double, double> _getVerticalSpacingForBlock( |
|
Block node, DefaultStyles defaultStyles) { |
|
Map<String, Attribute> attrs = node.style.attributes; |
|
if (attrs.containsKey(Attribute.blockQuote.key)) { |
|
return defaultStyles.quote.verticalSpacing; |
|
} else if (attrs.containsKey(Attribute.codeBlock.key)) { |
|
return defaultStyles.code.verticalSpacing; |
|
} else if (attrs.containsKey(Attribute.indent.key)) { |
|
return defaultStyles.indent.verticalSpacing; |
|
} |
|
return defaultStyles.lists.verticalSpacing; |
|
} |
|
|
|
@override |
|
void initState() { |
|
super.initState(); |
|
|
|
_clipboardStatus?.addListener(_onChangedClipboardStatus); |
|
|
|
widget.controller.addListener(_didChangeTextEditingValue); |
|
|
|
_scrollController = widget.scrollController ?? ScrollController(); |
|
_scrollController.addListener(_updateSelectionOverlayForScroll); |
|
|
|
_cursorCont = CursorCont( |
|
show: ValueNotifier<bool>(widget.showCursor ?? false), |
|
style: widget.cursorStyle ?? |
|
CursorStyle( |
|
color: Colors.blueAccent, |
|
backgroundColor: Colors.grey, |
|
width: 2.0, |
|
), |
|
tickerProvider: this, |
|
); |
|
|
|
_keyboardListener = KeyboardListener( |
|
handleCursorMovement, |
|
handleShortcut, |
|
handleDelete, |
|
); |
|
|
|
_focusAttachment = widget.focusNode.attach(context, |
|
onKey: (node, event) => _keyboardListener.handleRawKeyEvent(event)); |
|
widget.focusNode.addListener(_handleFocusChanged); |
|
} |
|
|
|
@override |
|
didChangeDependencies() { |
|
super.didChangeDependencies(); |
|
DefaultStyles parentStyles = QuillStyles.getStyles(context, true); |
|
DefaultStyles defaultStyles = DefaultStyles.getInstance(context); |
|
_styles = (parentStyles != null) |
|
? defaultStyles.merge(parentStyles) |
|
: defaultStyles; |
|
|
|
if (!_didAutoFocus && widget.autoFocus) { |
|
FocusScope.of(context).autofocus(widget.focusNode); |
|
_didAutoFocus = true; |
|
} |
|
} |
|
|
|
@override |
|
void didUpdateWidget(RawEditor oldWidget) { |
|
super.didUpdateWidget(oldWidget); |
|
|
|
_cursorCont.show.value = widget.showCursor; |
|
_cursorCont.style = widget.cursorStyle; |
|
|
|
if (widget.controller != oldWidget.controller) { |
|
oldWidget.controller.removeListener(_didChangeTextEditingValue); |
|
widget.controller.addListener(_didChangeTextEditingValue); |
|
updateRemoteValueIfNeeded(); |
|
} |
|
|
|
if (widget.scrollController != null && |
|
widget.scrollController != _scrollController) { |
|
_scrollController.removeListener(_updateSelectionOverlayForScroll); |
|
_scrollController = widget.scrollController; |
|
_scrollController.addListener(_updateSelectionOverlayForScroll); |
|
} |
|
|
|
if (widget.focusNode != oldWidget.focusNode) { |
|
oldWidget.focusNode.removeListener(_handleFocusChanged); |
|
_focusAttachment?.detach(); |
|
_focusAttachment = widget.focusNode.attach(context, |
|
onKey: (node, event) => _keyboardListener.handleRawKeyEvent(event)); |
|
widget.focusNode.addListener(_handleFocusChanged); |
|
updateKeepAlive(); |
|
} |
|
|
|
if (widget.controller.selection != oldWidget.controller.selection) { |
|
_selectionOverlay?.update(textEditingValue); |
|
} |
|
|
|
_selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); |
|
if (widget.readOnly) { |
|
closeConnectionIfNeeded(); |
|
} else { |
|
if (oldWidget.readOnly && _hasFocus) { |
|
openConnectionIfNeeded(); |
|
} |
|
} |
|
} |
|
|
|
bool _shouldShowSelectionHandles() { |
|
return widget.showSelectionHandles && |
|
!widget.controller.selection.isCollapsed; |
|
} |
|
|
|
handleDelete(bool forward) { |
|
TextSelection selection = widget.controller.selection; |
|
String plainText = textEditingValue.text; |
|
assert(selection != null); |
|
int cursorPosition = selection.start; |
|
String textBefore = selection.textBefore(plainText); |
|
String textAfter = selection.textAfter(plainText); |
|
if (selection.isCollapsed) { |
|
if (!forward && textBefore.isNotEmpty) { |
|
final int characterBoundary = |
|
_previousCharacter(textBefore.length, textBefore, true); |
|
textBefore = textBefore.substring(0, characterBoundary); |
|
cursorPosition = characterBoundary; |
|
} |
|
if (forward && textAfter.isNotEmpty && textAfter != '\n') { |
|
final int deleteCount = _nextCharacter(0, textAfter, true); |
|
textAfter = textAfter.substring(deleteCount); |
|
} |
|
} |
|
TextSelection newSelection = |
|
TextSelection.collapsed(offset: cursorPosition); |
|
String newText = textBefore + textAfter; |
|
int size = plainText.length - newText.length; |
|
widget.controller.replaceText( |
|
cursorPosition, |
|
size, |
|
'', |
|
newSelection, |
|
); |
|
} |
|
|
|
Future<void> handleShortcut(InputShortcut shortcut) async { |
|
TextSelection selection = widget.controller.selection; |
|
assert(selection != null); |
|
String plainText = textEditingValue.text; |
|
if (shortcut == InputShortcut.COPY) { |
|
if (!selection.isCollapsed) { |
|
Clipboard.setData(ClipboardData(text: selection.textInside(plainText))); |
|
} |
|
return; |
|
} |
|
if (shortcut == InputShortcut.CUT && !widget.readOnly) { |
|
if (!selection.isCollapsed) { |
|
final data = selection.textInside(plainText); |
|
Clipboard.setData(ClipboardData(text: data)); |
|
|
|
widget.controller.replaceText( |
|
selection.start, |
|
data.length, |
|
'', |
|
TextSelection.collapsed(offset: selection.start), |
|
); |
|
|
|
textEditingValue = TextEditingValue( |
|
text: |
|
selection.textBefore(plainText) + selection.textAfter(plainText), |
|
selection: TextSelection.collapsed(offset: selection.start), |
|
); |
|
} |
|
return; |
|
} |
|
if (shortcut == InputShortcut.PASTE && !widget.readOnly) { |
|
ClipboardData 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: textEditingValue.text.length, |
|
), |
|
ChangeSource.REMOTE); |
|
return; |
|
} |
|
} |
|
|
|
@override |
|
void dispose() { |
|
closeConnectionIfNeeded(); |
|
assert(!hasConnection); |
|
_selectionOverlay?.dispose(); |
|
_selectionOverlay = null; |
|
widget.controller.removeListener(_didChangeTextEditingValue); |
|
widget.focusNode.removeListener(_handleFocusChanged); |
|
_focusAttachment.detach(); |
|
_cursorCont.dispose(); |
|
_clipboardStatus?.removeListener(_onChangedClipboardStatus); |
|
_clipboardStatus?.dispose(); |
|
super.dispose(); |
|
} |
|
|
|
_updateSelectionOverlayForScroll() { |
|
_selectionOverlay?.markNeedsBuild(); |
|
} |
|
|
|
_didChangeTextEditingValue() { |
|
requestKeyboard(); |
|
|
|
_showCaretOnScreen(); |
|
updateRemoteValueIfNeeded(); |
|
_cursorCont.startOrStopCursorTimerIfNeeded( |
|
_hasFocus, widget.controller.selection); |
|
if (hasConnection) { |
|
_cursorCont.stopCursorTimer(resetCharTicks: false); |
|
_cursorCont.startCursorTimer(); |
|
} |
|
|
|
SchedulerBinding.instance.addPostFrameCallback( |
|
(Duration _) => _updateOrDisposeSelectionOverlayIfNeeded()); |
|
setState(() { |
|
// Use widget.controller.value in build() |
|
// Trigger build and updateChildren |
|
}); |
|
} |
|
|
|
_updateOrDisposeSelectionOverlayIfNeeded() { |
|
if (_selectionOverlay != null) { |
|
if (_hasFocus) { |
|
_selectionOverlay.update(textEditingValue); |
|
} else { |
|
_selectionOverlay.dispose(); |
|
_selectionOverlay = null; |
|
} |
|
} else if (_hasFocus) { |
|
_selectionOverlay?.hide(); |
|
_selectionOverlay = null; |
|
|
|
if (widget.selectionCtrls != null) { |
|
_selectionOverlay = EditorTextSelectionOverlay( |
|
textEditingValue, |
|
false, |
|
context, |
|
widget, |
|
_toolbarLayerLink, |
|
_startHandleLayerLink, |
|
_endHandleLayerLink, |
|
getRenderEditor(), |
|
widget.selectionCtrls, |
|
this, |
|
DragStartBehavior.start, |
|
null, |
|
_clipboardStatus); |
|
_selectionOverlay.handlesVisible = _shouldShowSelectionHandles(); |
|
_selectionOverlay.showHandles(); |
|
} |
|
} |
|
} |
|
|
|
_handleFocusChanged() { |
|
openOrCloseConnection(); |
|
_cursorCont.startOrStopCursorTimerIfNeeded( |
|
_hasFocus, widget.controller.selection); |
|
_updateOrDisposeSelectionOverlayIfNeeded(); |
|
if (_hasFocus) { |
|
WidgetsBinding.instance.addObserver(this); |
|
_showCaretOnScreen(); |
|
} else { |
|
WidgetsBinding.instance.removeObserver(this); |
|
} |
|
updateKeepAlive(); |
|
} |
|
|
|
_onChangedClipboardStatus() { |
|
setState(() { |
|
// Inform the widget that the value of clipboardStatus has changed. |
|
// Trigger build and updateChildren |
|
}); |
|
} |
|
|
|
bool _showCaretOnScreenScheduled = false; |
|
|
|
_showCaretOnScreen() { |
|
if (!widget.showCursor || _showCaretOnScreenScheduled) { |
|
return; |
|
} |
|
|
|
_showCaretOnScreenScheduled = true; |
|
SchedulerBinding.instance.addPostFrameCallback((Duration _) { |
|
_showCaretOnScreenScheduled = false; |
|
|
|
final viewport = RenderAbstractViewport.of(getRenderEditor()); |
|
assert(viewport != null); |
|
final editorOffset = |
|
getRenderEditor().localToGlobal(Offset(0.0, 0.0), ancestor: viewport); |
|
final offsetInViewport = _scrollController.offset + editorOffset.dy; |
|
|
|
final offset = getRenderEditor().getOffsetToRevealCursor( |
|
_scrollController.position.viewportDimension, |
|
_scrollController.offset, |
|
offsetInViewport, |
|
); |
|
|
|
if (offset != null) { |
|
_scrollController.animateTo( |
|
offset, |
|
duration: Duration(milliseconds: 100), |
|
curve: Curves.fastOutSlowIn, |
|
); |
|
} |
|
}); |
|
} |
|
|
|
@override |
|
RenderEditor getRenderEditor() { |
|
return _editorKey.currentContext.findRenderObject(); |
|
} |
|
|
|
@override |
|
EditorTextSelectionOverlay getSelectionOverlay() { |
|
return _selectionOverlay; |
|
} |
|
|
|
@override |
|
TextEditingValue getTextEditingValue() { |
|
return widget.controller.plainTextEditingValue; |
|
} |
|
|
|
@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 |
|
requestKeyboard() { |
|
if (_hasFocus) { |
|
openConnectionIfNeeded(); |
|
} else { |
|
widget.focusNode.requestFocus(); |
|
} |
|
} |
|
|
|
@override |
|
setTextEditingValue(TextEditingValue value) { |
|
widget.controller.updateSelection(value.selection, ChangeSource.LOCAL); |
|
} |
|
|
|
@override |
|
bool showToolbar() { |
|
if (_selectionOverlay == null || _selectionOverlay.toolbar != null) { |
|
return false; |
|
} |
|
|
|
_selectionOverlay.showToolbar(); |
|
return true; |
|
} |
|
|
|
@override |
|
bool get wantKeepAlive => widget.focusNode.hasFocus; |
|
|
|
openOrCloseConnection() { |
|
if (widget.focusNode.hasFocus && widget.focusNode.consumeKeyboardToken()) { |
|
openConnectionIfNeeded(); |
|
} else if (!widget.focusNode.hasFocus) { |
|
closeConnectionIfNeeded(); |
|
} |
|
} |
|
} |
|
|
|
class _Editor extends MultiChildRenderObjectWidget { |
|
_Editor({ |
|
@required Key key, |
|
@required List<Widget> children, |
|
@required this.document, |
|
@required this.textDirection, |
|
@required this.hasFocus, |
|
@required this.selection, |
|
@required this.startHandleLayerLink, |
|
@required this.endHandleLayerLink, |
|
@required this.onSelectionChanged, |
|
this.padding = EdgeInsets.zero, |
|
}) : super(key: key, children: children); |
|
|
|
final Document document; |
|
final TextDirection textDirection; |
|
final bool hasFocus; |
|
final TextSelection selection; |
|
final LayerLink startHandleLayerLink; |
|
final LayerLink endHandleLayerLink; |
|
final TextSelectionChangedHandler onSelectionChanged; |
|
final EdgeInsetsGeometry padding; |
|
|
|
@override |
|
RenderEditor createRenderObject(BuildContext context) { |
|
return RenderEditor( |
|
null, |
|
textDirection, |
|
padding, |
|
document, |
|
selection, |
|
hasFocus, |
|
onSelectionChanged, |
|
startHandleLayerLink, |
|
endHandleLayerLink, |
|
EdgeInsets.fromLTRB(4, 4, 4, 5)); |
|
} |
|
|
|
@override |
|
updateRenderObject( |
|
BuildContext context, covariant RenderEditor renderObject) { |
|
renderObject.document = document; |
|
renderObject.setContainer(document.root); |
|
renderObject.textDirection = textDirection; |
|
renderObject.setHasFocus(hasFocus); |
|
renderObject.setSelection(selection); |
|
renderObject.setStartHandleLayerLink(startHandleLayerLink); |
|
renderObject.setEndHandleLayerLink(endHandleLayerLink); |
|
renderObject.onSelectionChanged = onSelectionChanged; |
|
renderObject.setPadding(padding); |
|
} |
|
}
|
|
|