From abd305582c89ba14565af373bfbdf827c140c909 Mon Sep 17 00:00:00 2001 From: zhaoce Date: Tue, 8 Jun 2021 22:41:11 +0800 Subject: [PATCH] Revert "merge conflict (#257)" This reverts commit 6105134f8aabf25d589278e18f7cc2c39f89136c. --- .github/ISSUE_TEMPLATE/issue-template.md | 2 +- CHANGELOG.md | 6 - lib/models/documents/document.dart | 31 +- lib/models/rules/rule.dart | 8 +- lib/widgets/controller.dart | 4 +- lib/widgets/editor.dart | 32 +- lib/widgets/raw_editor.dart | 566 ++++++++++++++++-- .../raw_editor_state_keyboard_mixin.dart | 354 ----------- ...editor_state_selection_delegate_mixin.dart | 40 -- ..._editor_state_text_input_client_mixin.dart | 200 ------- lib/widgets/simple_viewer.dart | 344 ----------- lib/widgets/text_block.dart | 70 +-- pubspec.yaml | 2 +- 13 files changed, 578 insertions(+), 1081 deletions(-) delete mode 100644 lib/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart delete mode 100644 lib/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart delete mode 100644 lib/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart delete mode 100644 lib/widgets/simple_viewer.dart diff --git a/.github/ISSUE_TEMPLATE/issue-template.md b/.github/ISSUE_TEMPLATE/issue-template.md index 077b59c0..ff759a4b 100644 --- a/.github/ISSUE_TEMPLATE/issue-template.md +++ b/.github/ISSUE_TEMPLATE/issue-template.md @@ -13,4 +13,4 @@ My issue is about [Desktop] I have tried running `example` directory successfully before creating an issue here. -Please note that we are using stable channel on branch master. If you are using beta or master channel, use branch dev. +Please note that we are using stable channel. If you are using beta or master channel, those are not supported. diff --git a/CHANGELOG.md b/CHANGELOG.md index 323da506..d96c8386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,3 @@ -## [1.3.0] -* Support flutter 2.2.0. - -## [1.2.2] -* Checkbox supports tapping. - ## [1.2.1] * Indented position not holding while editing. diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index 9d966f32..68dbee4d 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -40,10 +40,6 @@ class Document { final Rules _rules = Rules.getInstance(); - void setCustomRules(List customRules) { - _rules.setCustomRules(customRules); - } - final StreamController> _observer = StreamController.broadcast(); @@ -51,7 +47,7 @@ class Document { Stream> get changes => _observer.stream; - Delta insert(int index, Object? data, {int replaceLength = 0, bool autoAppendNewlineAfterImage = true}) { + Delta insert(int index, Object? data, {int replaceLength = 0}) { assert(index >= 0); assert(data is String || data is Embeddable); if (data is Embeddable) { @@ -62,7 +58,7 @@ class Document { final delta = _rules.apply(RuleType.INSERT, this, index, data: data, len: replaceLength); - compose(delta, ChangeSource.LOCAL, autoAppendNewlineAfterImage: autoAppendNewlineAfterImage); + compose(delta, ChangeSource.LOCAL); return delta; } @@ -75,7 +71,7 @@ class Document { return delta; } - Delta replace(int index, int len, Object? data, {bool autoAppendNewlineAfterImage = true}) { + Delta replace(int index, int len, Object? data) { assert(index >= 0); assert(data is String || data is Embeddable); @@ -88,8 +84,7 @@ class Document { // We have to insert before applying delete rules // Otherwise delete would be operating on stale document snapshot. if (dataIsNotEmpty) { - delta = insert(index, data, replaceLength: len, - autoAppendNewlineAfterImage: autoAppendNewlineAfterImage); + delta = insert(index, data, replaceLength: len); } if (len > 0) { @@ -129,13 +124,13 @@ class Document { return block.queryChild(res.offset, true); } - void compose(Delta delta, ChangeSource changeSource, {bool autoAppendNewlineAfterImage = true}) { + void compose(Delta delta, ChangeSource changeSource) { assert(!_observer.isClosed); delta.trim(); assert(delta.isNotEmpty); var offset = 0; - delta = _transform(delta, autoAppendNewlineAfterImage: autoAppendNewlineAfterImage); + delta = _transform(delta); final originalDelta = toDelta(); for (final op in delta.toList()) { final style = @@ -179,28 +174,22 @@ class Document { bool get hasRedo => _history.hasRedo; - static Delta _transform(Delta delta, {bool autoAppendNewlineAfterImage = true}) { + static Delta _transform(Delta delta) { final res = Delta(); final ops = delta.toList(); for (var i = 0; i < ops.length; i++) { final op = ops[i]; res.push(op); - if (autoAppendNewlineAfterImage) { - _autoAppendNewlineAfterImage(i, ops, op, res); - } + _handleImageInsert(i, ops, op, res); } return res; } - static void _autoAppendNewlineAfterImage( + static void _handleImageInsert( int i, List ops, Operation op, Delta res) { final nextOpIsImage = i + 1 < ops.length && ops[i + 1].isInsert && ops[i + 1].data is! String; - if (nextOpIsImage && - op.data is String && - (op.data as String).isNotEmpty && - !(op.data as String).endsWith('\n')) - { + if (nextOpIsImage && !(op.data as String).endsWith('\n')) { res.push(Operation.insert('\n')); } // Currently embed is equivalent to image and hence `is! String` diff --git a/lib/models/rules/rule.dart b/lib/models/rules/rule.dart index 042f1aaa..4ee6c278 100644 --- a/lib/models/rules/rule.dart +++ b/lib/models/rules/rule.dart @@ -28,8 +28,6 @@ abstract class Rule { class Rules { Rules(this._rules); - List _customRules = []; - final List _rules; static final Rules _instance = Rules([ const FormatLinkAtCaretPositionRule(), @@ -51,14 +49,10 @@ class Rules { static Rules getInstance() => _instance; - void setCustomRules(List customRules) { - _customRules = customRules; - } - Delta apply(RuleType ruleType, Document document, int index, {int? len, Object? data, Attribute? attribute}) { final delta = document.toDelta(); - for (final rule in _customRules + _rules) { + for (final rule in _rules) { if (rule.type != ruleType) { continue; } diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index 4820b3f9..cb5136df 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -92,12 +92,12 @@ class QuillController extends ChangeNotifier { void replaceText( int index, int len, Object? data, TextSelection? textSelection, - {bool ignoreFocus = false, bool autoAppendNewlineAfterImage = true}) { + {bool ignoreFocus = false}) { assert(data is String || data is Embeddable); Delta? delta; if (len > 0 || data is! String || data.isNotEmpty) { - delta = document.replace(index, len, data, autoAppendNewlineAfterImage: autoAppendNewlineAfterImage); + delta = document.replace(index, len, data); var shouldRetainDelta = toggledStyle.isNotEmpty && delta.isNotEmpty && delta.length <= 2 && diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 662a018c..f021b88a 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -393,6 +393,8 @@ class _QuillEditorSelectionGestureDetectorBuilder final segmentResult = line.queryChild(result.offset, false); if (segmentResult.node == null) { if (line.length == 1) { + // tapping when no text yet on this line + _flipListCheckbox(pos, line, segmentResult); getEditor()!.widget.controller.updateSelection( TextSelection.collapsed(offset: pos.offset), ChangeSource.LOCAL); return true; @@ -432,11 +434,39 @@ class _QuillEditorSelectionGestureDetectorBuilder ), ); } + return false; + } + if (_flipListCheckbox(pos, line, segmentResult)) { + return true; } - return false; } + bool _flipListCheckbox( + TextPosition pos, Line line, container_node.ChildQuery segmentResult) { + if (getEditor()!.widget.readOnly || + !line.style.containsKey(Attribute.list.key) || + segmentResult.offset != 0) { + return false; + } + // segmentResult.offset == 0 means tap at the beginning of the TextLine + final String? listVal = line.style.attributes[Attribute.list.key]!.value; + if (listVal == Attribute.unchecked.value) { + getEditor()! + .widget + .controller + .formatText(pos.offset, 0, Attribute.checked); + } else if (listVal == Attribute.checked.value) { + getEditor()! + .widget + .controller + .formatText(pos.offset, 0, Attribute.unchecked); + } + getEditor()!.widget.controller.updateSelection( + TextSelection.collapsed(offset: pos.offset), ChangeSource.LOCAL); + return true; + } + Future _launchUrl(String url) async { await launch(url); } diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 37c22819..022a140d 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -15,6 +15,7 @@ import '../models/documents/attribute.dart'; import '../models/documents/document.dart'; import '../models/documents/nodes/block.dart'; import '../models/documents/nodes/line.dart'; +import '../utils/diff_delta.dart'; import 'controller.dart'; import 'cursor.dart'; import 'default_styles.dart'; @@ -22,9 +23,6 @@ import 'delegate.dart'; import 'editor.dart'; import 'keyboard_listener.dart'; import 'proxy.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'; import 'text_line.dart'; import 'text_selection.dart'; @@ -91,55 +89,406 @@ class RawEditor extends StatefulWidget { final EmbedBuilder embedBuilder; @override - State createState() => RawEditorState(); + State createState() { + return RawEditorState(); + } } class RawEditorState extends EditorState with AutomaticKeepAliveClientMixin, WidgetsBindingObserver, - TickerProviderStateMixin, - RawEditorStateKeyboardMixin, - RawEditorStateTextInputClientMixin, - RawEditorStateSelectionDelegateMixin { + TickerProviderStateMixin + implements TextSelectionDelegate, TextInputClient { final GlobalKey _editorKey = GlobalKey(); - - // Keyboard - late KeyboardListener _keyboardListener; + final List _sentRemoteValues = []; + TextInputConnection? _textInputConnection; + TextEditingValue? _lastKnownRemoteTextEditingValue; + int _cursorResetLocation = -1; + bool _wasSelectingVerticallyWithKeyboard = false; + EditorTextSelectionOverlay? _selectionOverlay; + FocusAttachment? _focusAttachment; + late CursorCont _cursorCont; + ScrollController? _scrollController; KeyboardVisibilityController? _keyboardVisibilityController; StreamSubscription? _keyboardVisibilitySubscription; + late KeyboardListener _keyboardListener; + bool _didAutoFocus = false; bool _keyboardVisible = false; + DefaultStyles? _styles; + final ClipboardStatusNotifier? _clipboardStatus = + kIsWeb ? null : ClipboardStatusNotifier(); + final LayerLink _toolbarLayerLink = LayerLink(); + final LayerLink _startHandleLayerLink = LayerLink(); + final LayerLink _endHandleLayerLink = LayerLink(); + + /// Whether to create an input connection with the platform for text editing + /// or not. + /// + /// Read-only input fields do not need a connection with the platform since + /// there's no need for text editing capabilities (e.g. virtual keyboard). + /// + /// On the web, we always need a connection because we want some browser + /// functionalities to continue to work on read-only input fields like: + /// + /// - Relevant context menu. + /// - cmd/ctrl+c shortcut to copy. + /// - cmd/ctrl+a to select all. + /// - Changing the selection using a physical keyboard. + bool get shouldCreateInputConnection => kIsWeb || !widget.readOnly; + + bool get _hasFocus => widget.focusNode.hasFocus; + + TextDirection get _textDirection { + final result = Directionality.of(context); + return result; + } + + void handleCursorMovement( + LogicalKeyboardKey key, + bool wordModifier, + bool lineModifier, + bool shift, + ) { + if (wordModifier && lineModifier) { + return; + } + final selection = widget.controller.selection; + + var newSelection = widget.controller.selection; + + final plainText = textEditingValue.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); + } + + 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)); + } + + 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 _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; + } + + 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; + } + + 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; + } + + bool get hasConnection => + _textInputConnection != null && _textInputConnection!.attached; + + void openConnectionIfNeeded() { + if (!shouldCreateInputConnection) { + return; + } + + if (!hasConnection) { + _lastKnownRemoteTextEditingValue = textEditingValue; + _textInputConnection = TextInput.attach( + this, + TextInputConfiguration( + inputType: TextInputType.multiline, + readOnly: widget.readOnly, + inputAction: TextInputAction.newline, + enableSuggestions: !widget.readOnly, + keyboardAppearance: widget.keyboardAppearance, + textCapitalization: widget.textCapitalization, + ), + ); + + _textInputConnection!.setEditingState(_lastKnownRemoteTextEditingValue!); + // _sentRemoteValues.add(_lastKnownRemoteTextEditingValue); + } + + _textInputConnection!.show(); + } + + void closeConnectionIfNeeded() { + if (!hasConnection) { + return; + } + _textInputConnection!.close(); + _textInputConnection = null; + _lastKnownRemoteTextEditingValue = null; + _sentRemoteValues.clear(); + } + + void updateRemoteValueIfNeeded() { + if (!hasConnection) { + return; + } + + final actualValue = textEditingValue.copyWith( + composing: _lastKnownRemoteTextEditingValue!.composing, + ); + + if (actualValue == _lastKnownRemoteTextEditingValue) { + return; + } + + final shouldRemember = + textEditingValue.text != _lastKnownRemoteTextEditingValue!.text; + _lastKnownRemoteTextEditingValue = actualValue; + _textInputConnection!.setEditingState(actualValue); + if (shouldRemember) { + _sentRemoteValues.add(actualValue); + } + } - // Selection overlay @override - EditorTextSelectionOverlay? getSelectionOverlay() => _selectionOverlay; - EditorTextSelectionOverlay? _selectionOverlay; + TextEditingValue? get currentTextEditingValue => + _lastKnownRemoteTextEditingValue; - ScrollController? _scrollController; + @override + AutofillScope? get currentAutofillScope => null; - late CursorCont _cursorCont; + @override + void updateEditingValue(TextEditingValue value) { + if (!shouldCreateInputConnection) { + return; + } - // Focus - bool _didAutoFocus = false; - FocusAttachment? _focusAttachment; - bool get _hasFocus => widget.focusNode.hasFocus; + if (_sentRemoteValues.contains(value)) { + _sentRemoteValues.remove(value); + return; + } - DefaultStyles? _styles; + if (_lastKnownRemoteTextEditingValue == value) { + return; + } + + if (_lastKnownRemoteTextEditingValue!.text == value.text && + _lastKnownRemoteTextEditingValue!.selection == value.selection) { + _lastKnownRemoteTextEditingValue = value; + return; + } + + final effectiveLastKnownValue = _lastKnownRemoteTextEditingValue!; + _lastKnownRemoteTextEditingValue = value; + final oldText = effectiveLastKnownValue.text; + final text = value.text; + final cursorPosition = value.selection.extentOffset; + final diff = getDiff(oldText, text, cursorPosition); + widget.controller.replaceText( + diff.start, diff.deleted.length, diff.inserted, value.selection); + } @override - void bringIntoView(TextPosition position) { - setState(() { - widget.controller.selection = TextSelection( - baseOffset: widget.controller.selection.baseOffset, - extentOffset: position.offset); - }); + TextEditingValue get textEditingValue { + return getTextEditingValue(); + } + + @override + set textEditingValue(TextEditingValue value) { + setTextEditingValue(value); + } + + @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(); } - final ClipboardStatusNotifier? _clipboardStatus = - kIsWeb ? null : ClipboardStatusNotifier(); - final LayerLink _toolbarLayerLink = LayerLink(); - final LayerLink _startHandleLayerLink = LayerLink(); - final LayerLink _endHandleLayerLink = LayerLink(); @override void bringIntoView(TextPosition position) { setState(() { @@ -149,7 +498,16 @@ class RawEditorState extends EditorState }); } - TextDirection get _textDirection => Directionality.of(context); + @override + void connectionClosed() { + if (!hasConnection) { + return; + } + _textInputConnection!.connectionClosedReceived(); + _textInputConnection = null; + _lastKnownRemoteTextEditingValue = null; + _sentRemoteValues.clear(); + } @override Widget build(BuildContext context) { @@ -227,18 +585,6 @@ class RawEditorState extends EditorState } } - /// Updates the checkbox positioned at [offset] in document - /// by changing its attribute according to [value]. - void _handleCheckboxTap(int offset, bool value) { - if (!widget.readOnly) { - if (value) { - widget.controller.formatText(offset, 0, Attribute.checked); - } else { - widget.controller.formatText(offset, 0, Attribute.unchecked); - } - } - } - List _buildChildren(Document doc, BuildContext context) { final result = []; final indentLevelCounts = {}; @@ -249,23 +595,21 @@ class RawEditorState extends EditorState } else if (node is Block) { final attrs = node.style.attributes; final editableTextBlock = EditableTextBlock( - node, - _textDirection, - widget.scrollBottomInset, - _getVerticalSpacingForBlock(node, _styles), - widget.controller.selection, - widget.selectionColor, - _styles, - widget.enableInteractiveSelection, - _hasFocus, - attrs.containsKey(Attribute.codeBlock.key) - ? const EdgeInsets.all(16) - : null, - widget.embedBuilder, - _cursorCont, - indentLevelCounts, - _handleCheckboxTap, - ); + node, + _textDirection, + widget.scrollBottomInset, + _getVerticalSpacingForBlock(node, _styles), + widget.controller.selection, + widget.selectionColor, + _styles, + widget.enableInteractiveSelection, + _hasFocus, + attrs.containsKey(Attribute.codeBlock.key) + ? const EdgeInsets.all(16) + : null, + widget.embedBuilder, + _cursorCont, + indentLevelCounts); result.add(editableTextBlock); } else { throw StateError('Unreachable.'); @@ -444,6 +788,89 @@ class RawEditorState extends EditorState !widget.controller.selection.isCollapsed; } + void handleDelete(bool forward) { + final selection = widget.controller.selection; + final plainText = textEditingValue.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; + widget.controller.replaceText( + cursorPosition, + size, + '', + newSelection, + ); + } + + Future handleShortcut(InputShortcut? shortcut) async { + final selection = widget.controller.selection; + final plainText = textEditingValue.text; + if (shortcut == InputShortcut.COPY) { + if (!selection.isCollapsed) { + await Clipboard.setData( + ClipboardData(text: selection.textInside(plainText))); + } + 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), + ); + + textEditingValue = TextEditingValue( + text: + selection.textBefore(plainText) + selection.textAfter(plainText), + selection: TextSelection.collapsed(offset: selection.start), + ); + } + 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: textEditingValue.text.length, + ), + ChangeSource.REMOTE); + return; + } + } + @override void dispose() { closeConnectionIfNeeded(); @@ -604,6 +1031,11 @@ class RawEditorState extends EditorState return _editorKey.currentContext!.findRenderObject() as RenderEditor?; } + @override + EditorTextSelectionOverlay? getSelectionOverlay() { + return _selectionOverlay; + } + @override TextEditingValue getTextEditingValue() { return widget.controller.plainTextEditingValue; @@ -705,10 +1137,12 @@ class RawEditorState extends EditorState @override bool get wantKeepAlive => widget.focusNode.hasFocus; - @override - void userUpdateTextEditingValue( - TextEditingValue value, SelectionChangedCause cause) { - // TODO: implement userUpdateTextEditingValue + void openOrCloseConnection() { + if (widget.focusNode.hasFocus && widget.focusNode.consumeKeyboardToken()) { + openConnectionIfNeeded(); + } else if (!widget.focusNode.hasFocus) { + closeConnectionIfNeeded(); + } } @override 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 0eb7f955..00000000 --- a/lib/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart +++ /dev/null @@ -1,354 +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) - 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.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), - )); - } - 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; - 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/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart b/lib/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart deleted file mode 100644 index cda991cc..00000000 --- a/lib/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/widgets.dart'; - -import '../editor.dart'; - -mixin RawEditorStateSelectionDelegateMixin on EditorState - implements TextSelectionDelegate { - @override - TextEditingValue get textEditingValue { - return getTextEditingValue(); - } - - @override - set textEditingValue(TextEditingValue value) { - setTextEditingValue(value); - } - - @override - void bringIntoView(TextPosition position) { - // TODO: implement bringIntoView - } - - @override - void hideToolbar([bool hideHandles = true]) { - 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; -} diff --git a/lib/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart b/lib/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart deleted file mode 100644 index 527df582..00000000 --- a/lib/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart +++ /dev/null @@ -1,200 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import '../../utils/diff_delta.dart'; -import '../editor.dart'; - -mixin RawEditorStateTextInputClientMixin on EditorState - implements TextInputClient { - final List _sentRemoteValues = []; - TextInputConnection? _textInputConnection; - TextEditingValue? _lastKnownRemoteTextEditingValue; - - /// Whether to create an input connection with the platform for text editing - /// or not. - /// - /// Read-only input fields do not need a connection with the platform since - /// there's no need for text editing capabilities (e.g. virtual keyboard). - /// - /// On the web, we always need a connection because we want some browser - /// functionalities to continue to work on read-only input fields like: - /// - /// - Relevant context menu. - /// - cmd/ctrl+c shortcut to copy. - /// - cmd/ctrl+a to select all. - /// - Changing the selection using a physical keyboard. - bool get shouldCreateInputConnection => kIsWeb || !widget.readOnly; - - /// Returns `true` if there is open input connection. - bool get hasConnection => - _textInputConnection != null && _textInputConnection!.attached; - - /// Opens or closes input connection based on the current state of - /// [focusNode] and [value]. - void openOrCloseConnection() { - if (widget.focusNode.hasFocus && widget.focusNode.consumeKeyboardToken()) { - openConnectionIfNeeded(); - } else if (!widget.focusNode.hasFocus) { - closeConnectionIfNeeded(); - } - } - - void openConnectionIfNeeded() { - if (!shouldCreateInputConnection) { - return; - } - - if (!hasConnection) { - _lastKnownRemoteTextEditingValue = getTextEditingValue(); - _textInputConnection = TextInput.attach( - this, - TextInputConfiguration( - inputType: TextInputType.multiline, - readOnly: widget.readOnly, - inputAction: TextInputAction.newline, - enableSuggestions: !widget.readOnly, - keyboardAppearance: widget.keyboardAppearance, - textCapitalization: widget.textCapitalization, - ), - ); - - _textInputConnection!.setEditingState(_lastKnownRemoteTextEditingValue!); - // _sentRemoteValues.add(_lastKnownRemoteTextEditingValue); - } - - _textInputConnection!.show(); - } - - /// Closes input connection if it's currently open. Otherwise does nothing. - void closeConnectionIfNeeded() { - if (!hasConnection) { - return; - } - _textInputConnection!.close(); - _textInputConnection = null; - _lastKnownRemoteTextEditingValue = null; - _sentRemoteValues.clear(); - } - - /// Updates remote value based on current state of [document] and - /// [selection]. - /// - /// This method may not actually send an update to native side if it thinks - /// remote value is up to date or identical. - void updateRemoteValueIfNeeded() { - if (!hasConnection) { - return; - } - - // 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( - composing: _lastKnownRemoteTextEditingValue!.composing, - ); - - if (actualValue == _lastKnownRemoteTextEditingValue) { - return; - } - - final shouldRemember = - getTextEditingValue().text != _lastKnownRemoteTextEditingValue!.text; - _lastKnownRemoteTextEditingValue = actualValue; - _textInputConnection!.setEditingState(actualValue); - if (shouldRemember) { - // Only keep track if text changed (selection changes are not relevant) - _sentRemoteValues.add(actualValue); - } - } - - @override - TextEditingValue? get currentTextEditingValue => - _lastKnownRemoteTextEditingValue; - - // autofill is not needed - @override - AutofillScope? get currentAutofillScope => null; - - @override - void updateEditingValue(TextEditingValue value) { - if (!shouldCreateInputConnection) { - return; - } - - if (_sentRemoteValues.contains(value)) { - /// There is a race condition in Flutter text input plugin where sending - /// updates to native side too often results in broken behavior. - /// TextInputConnection.setEditingValue is an async call to native side. - /// For each such call native side _always_ sends an update which triggers - /// this method (updateEditingValue) with the same value we've sent it. - /// If multiple calls to setEditingValue happen too fast and we only - /// track the last sent value then there is no way for us to filter out - /// automatic callbacks from native side. - /// Therefore we have to keep track of all values we send to the native - /// side and when we see this same value appear here we skip it. - /// This is fragile but it's probably the only available option. - _sentRemoteValues.remove(value); - return; - } - - if (_lastKnownRemoteTextEditingValue == value) { - // There is no difference between this value and the last known value. - return; - } - - // Check if only composing range changed. - if (_lastKnownRemoteTextEditingValue!.text == value.text && - _lastKnownRemoteTextEditingValue!.selection == value.selection) { - // This update only modifies composing range. Since we don't keep track - // of composing range we just need to update last known value here. - // This check fixes an issue on Android when it sends - // composing updates separately from regular changes for text and - // selection. - _lastKnownRemoteTextEditingValue = value; - return; - } - - final effectiveLastKnownValue = _lastKnownRemoteTextEditingValue!; - _lastKnownRemoteTextEditingValue = value; - final oldText = effectiveLastKnownValue.text; - final text = value.text; - final cursorPosition = value.selection.extentOffset; - final diff = getDiff(oldText, text, cursorPosition); - widget.controller.replaceText( - diff.start, diff.deleted.length, diff.inserted, value.selection); - } - - @override - void performAction(TextInputAction action) { - // no-op - } - - @override - void performPrivateCommand(String action, Map data) { - // no-op - } - - @override - void updateFloatingCursor(RawFloatingCursorPoint point) { - throw UnimplementedError(); - } - - @override - void showAutocorrectionPromptRect(int start, int end) { - throw UnimplementedError(); - } - - @override - void connectionClosed() { - if (!hasConnection) { - return; - } - _textInputConnection!.connectionClosedReceived(); - _textInputConnection = null; - _lastKnownRemoteTextEditingValue = null; - _sentRemoteValues.clear(); - } -} diff --git a/lib/widgets/simple_viewer.dart b/lib/widgets/simple_viewer.dart deleted file mode 100644 index ee1a7732..00000000 --- a/lib/widgets/simple_viewer.dart +++ /dev/null @@ -1,344 +0,0 @@ -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:string_validator/string_validator.dart'; -import 'package:tuple/tuple.dart'; - -import '../models/documents/attribute.dart'; -import '../models/documents/document.dart'; -import '../models/documents/nodes/block.dart'; -import '../models/documents/nodes/leaf.dart' as leaf; -import '../models/documents/nodes/line.dart'; -import 'controller.dart'; -import 'cursor.dart'; -import 'default_styles.dart'; -import 'delegate.dart'; -import 'editor.dart'; -import 'text_block.dart'; -import 'text_line.dart'; - -class QuillSimpleViewer extends StatefulWidget { - const QuillSimpleViewer({ - required this.controller, - this.customStyles, - this.truncate = false, - this.truncateScale, - this.truncateAlignment, - this.truncateHeight, - this.truncateWidth, - this.scrollBottomInset = 0, - this.padding = EdgeInsets.zero, - this.embedBuilder, - Key? key, - }) : assert(truncate || - ((truncateScale == null) && - (truncateAlignment == null) && - (truncateHeight == null) && - (truncateWidth == null))), - super(key: key); - - final QuillController controller; - final DefaultStyles? customStyles; - final bool truncate; - final double? truncateScale; - final Alignment? truncateAlignment; - final double? truncateHeight; - final double? truncateWidth; - final double scrollBottomInset; - final EdgeInsetsGeometry padding; - final EmbedBuilder? embedBuilder; - - @override - _QuillSimpleViewerState createState() => _QuillSimpleViewerState(); -} - -class _QuillSimpleViewerState extends State - with SingleTickerProviderStateMixin { - late DefaultStyles _styles; - final LayerLink _toolbarLayerLink = LayerLink(); - final LayerLink _startHandleLayerLink = LayerLink(); - final LayerLink _endHandleLayerLink = LayerLink(); - late CursorCont _cursorCont; - - @override - void initState() { - super.initState(); - - _cursorCont = CursorCont( - show: ValueNotifier(false), - style: const CursorStyle( - color: Colors.black, - backgroundColor: Colors.grey, - width: 2, - radius: Radius.zero, - offset: Offset.zero, - ), - tickerProvider: this, - ); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final parentStyles = QuillStyles.getStyles(context, true); - final defaultStyles = DefaultStyles.getInstance(context); - _styles = (parentStyles != null) - ? defaultStyles.merge(parentStyles) - : defaultStyles; - - if (widget.customStyles != null) { - _styles = _styles.merge(widget.customStyles!); - } - } - - EmbedBuilder get embedBuilder => widget.embedBuilder ?? _defaultEmbedBuilder; - - Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) { - assert(!kIsWeb, 'Please provide EmbedBuilder for Web'); - switch (node.value.type) { - case 'image': - final imageUrl = _standardizeImageUrl(node.value.data); - return imageUrl.startsWith('http') - ? Image.network(imageUrl) - : isBase64(imageUrl) - ? Image.memory(base64.decode(imageUrl)) - : Image.file(io.File(imageUrl)); - default: - throw UnimplementedError( - 'Embeddable type "${node.value.type}" is not supported by default embed ' - 'builder of QuillEditor. You must pass your own builder function to ' - 'embedBuilder property of QuillEditor or QuillField widgets.'); - } - } - - String _standardizeImageUrl(String url) { - if (url.contains('base64')) { - return url.split(',')[1]; - } - return url; - } - - @override - Widget build(BuildContext context) { - final _doc = widget.controller.document; - // if (_doc.isEmpty() && - // !widget.focusNode.hasFocus && - // widget.placeholder != null) { - // _doc = Document.fromJson(jsonDecode( - // '[{"attributes":{"placeholder":true},"insert":"${widget.placeholder}\\n"}]')); - // } - - Widget child = CompositedTransformTarget( - link: _toolbarLayerLink, - child: Semantics( - child: _SimpleViewer( - document: _doc, - textDirection: _textDirection, - startHandleLayerLink: _startHandleLayerLink, - endHandleLayerLink: _endHandleLayerLink, - onSelectionChanged: _nullSelectionChanged, - scrollBottomInset: widget.scrollBottomInset, - padding: widget.padding, - children: _buildChildren(_doc, context), - ), - ), - ); - - if (widget.truncate) { - if (widget.truncateScale != null) { - child = Container( - height: widget.truncateHeight, - child: Align( - heightFactor: widget.truncateScale, - widthFactor: widget.truncateScale, - alignment: widget.truncateAlignment ?? Alignment.topLeft, - child: Container( - width: widget.truncateWidth! / widget.truncateScale!, - child: SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), - child: Transform.scale( - scale: widget.truncateScale!, - alignment: - widget.truncateAlignment ?? Alignment.topLeft, - child: child))))); - } else { - child = Container( - height: widget.truncateHeight, - width: widget.truncateWidth, - child: SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), child: child)); - } - } - - return QuillStyles(data: _styles, child: child); - } - - List _buildChildren(Document doc, BuildContext context) { - final result = []; - final indentLevelCounts = {}; - for (final node in doc.root.children) { - if (node is Line) { - final editableTextLine = _getEditableTextLineFromNode(node, context); - result.add(editableTextLine); - } else if (node is Block) { - final attrs = node.style.attributes; - final editableTextBlock = EditableTextBlock( - node, - _textDirection, - widget.scrollBottomInset, - _getVerticalSpacingForBlock(node, _styles), - widget.controller.selection, - Colors.black, - // selectionColor, - _styles, - false, - // enableInteractiveSelection, - false, - // hasFocus, - attrs.containsKey(Attribute.codeBlock.key) - ? const EdgeInsets.all(16) - : null, - embedBuilder, - _cursorCont, - indentLevelCounts, - _handleCheckboxTap); - result.add(editableTextBlock); - } else { - throw StateError('Unreachable.'); - } - } - return result; - } - - /// Updates the checkbox positioned at [offset] in document - /// by changing its attribute according to [value]. - void _handleCheckboxTap(int offset, bool value) { - // readonly - do nothing - } - - TextDirection get _textDirection { - final result = Directionality.of(context); - return result; - } - - EditableTextLine _getEditableTextLineFromNode( - Line node, BuildContext context) { - final textLine = TextLine( - line: node, - textDirection: _textDirection, - embedBuilder: embedBuilder, - styles: _styles, - ); - final editableTextLine = EditableTextLine( - node, - null, - textLine, - 0, - _getVerticalSpacingForLine(node, _styles), - _textDirection, - widget.controller.selection, - Colors.black, - //widget.selectionColor, - false, - //enableInteractiveSelection, - false, - //_hasFocus, - MediaQuery.of(context).devicePixelRatio, - _cursorCont); - return editableTextLine; - } - - Tuple2 _getVerticalSpacingForLine( - Line line, DefaultStyles? defaultStyles) { - final attrs = line.style.attributes; - if (attrs.containsKey(Attribute.header.key)) { - final 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 _getVerticalSpacingForBlock( - Block node, DefaultStyles? defaultStyles) { - final 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; - } - - void _nullSelectionChanged( - TextSelection selection, SelectionChangedCause cause) {} -} - -class _SimpleViewer extends MultiChildRenderObjectWidget { - _SimpleViewer({ - required List children, - required this.document, - required this.textDirection, - required this.startHandleLayerLink, - required this.endHandleLayerLink, - required this.onSelectionChanged, - required this.scrollBottomInset, - this.padding = EdgeInsets.zero, - Key? key, - }) : super(key: key, children: children); - - final Document document; - final TextDirection textDirection; - final LayerLink startHandleLayerLink; - final LayerLink endHandleLayerLink; - final TextSelectionChangedHandler onSelectionChanged; - final double scrollBottomInset; - final EdgeInsetsGeometry padding; - - @override - RenderEditor createRenderObject(BuildContext context) { - return RenderEditor( - null, - textDirection, - scrollBottomInset, - padding, - document, - const TextSelection(baseOffset: 0, extentOffset: 0), - false, - // hasFocus, - onSelectionChanged, - startHandleLayerLink, - endHandleLayerLink, - const EdgeInsets.fromLTRB(4, 4, 4, 5), - ); - } - - @override - void updateRenderObject( - BuildContext context, covariant RenderEditor renderObject) { - renderObject - ..document = document - ..setContainer(document.root) - ..textDirection = textDirection - ..setStartHandleLayerLink(startHandleLayerLink) - ..setEndHandleLayerLink(endHandleLayerLink) - ..onSelectionChanged = onSelectionChanged - ..setScrollBottomInset(scrollBottomInset) - ..setPadding(padding); - } -} diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index f533a160..309b9cf8 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -61,7 +61,6 @@ class EditableTextBlock extends StatelessWidget { this.embedBuilder, this.cursorCont, this.indentLevelCounts, - this.onCheckboxTap, ); final Block block; @@ -77,7 +76,6 @@ class EditableTextBlock extends StatelessWidget { final EmbedBuilder embedBuilder; final CursorCont cursorCont; final Map indentLevelCounts; - final Function(int, bool) onCheckboxTap; @override Widget build(BuildContext context) { @@ -163,23 +161,12 @@ class EditableTextBlock extends StatelessWidget { if (attrs[Attribute.list.key] == Attribute.checked) { return _Checkbox( - key: UniqueKey(), - style: defaultStyles!.leading!.style, - width: 32, - isChecked: true, - offset: block.offset + line.offset, - onTap: onCheckboxTap, - ); + style: defaultStyles!.leading!.style, width: 32, isChecked: true); } if (attrs[Attribute.list.key] == Attribute.unchecked) { return _Checkbox( - key: UniqueKey(), - style: defaultStyles!.leading!.style, - width: 32, - offset: block.offset + line.offset, - onTap: onCheckboxTap, - ); + style: defaultStyles!.leading!.style, width: 32, isChecked: false); } if (attrs.containsKey(Attribute.codeBlock.key)) { @@ -698,39 +685,46 @@ class _BulletPoint extends StatelessWidget { } } -class _Checkbox extends StatelessWidget { - const _Checkbox({ - Key? key, - this.style, - this.width, - this.isChecked = false, - this.offset, - this.onTap, - }) : super(key: key); +class _Checkbox extends StatefulWidget { + const _Checkbox({Key? key, this.style, this.width, this.isChecked}) + : super(key: key); + final TextStyle? style; final double? width; - final bool isChecked; - final int? offset; - final Function(int, bool)? onTap; + final bool? isChecked; - void _onCheckboxClicked(bool? newValue) { - if (onTap != null && newValue != null && offset != null) { - onTap!(offset!, newValue); - } + @override + __CheckboxState createState() => __CheckboxState(); +} + +class __CheckboxState extends State<_Checkbox> { + bool? isChecked; + + void _onCheckboxClicked(bool? newValue) => setState(() { + isChecked = newValue; + + if (isChecked!) { + // check list + } else { + // uncheck list + } + }); + + @override + void initState() { + super.initState(); + isChecked = widget.isChecked; } @override Widget build(BuildContext context) { return Container( alignment: AlignmentDirectional.topEnd, - width: width, + width: widget.width, padding: const EdgeInsetsDirectional.only(end: 13), - child: GestureDetector( - onLongPress: () => _onCheckboxClicked(!isChecked), - child: Checkbox( - value: isChecked, - onChanged: _onCheckboxClicked, - ), + child: Checkbox( + value: widget.isChecked, + onChanged: _onCheckboxClicked, ), ); } diff --git a/pubspec.yaml b/pubspec.yaml index 7cf52633..5e87f6d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 1.3.0 +version: 1.2.1 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill