diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e8c2dee..80ce4dae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [7.0.0] +* Breaking change: Tuples are no longer used. They have been replaced with a number of data classes. + # [6.4.4] * Increased compatibility with Flutter widget tests. diff --git a/README.md b/README.md index bd809b78..cd772c47 100644 --- a/README.md +++ b/README.md @@ -319,7 +319,7 @@ Future _addEditNote(BuildContext context, {Document? document}) async { final length = controller.selection.extentOffset - index; if (isEditing) { - final offset = getEmbedNode(controller, controller.selection.start).item1; + final offset = getEmbedNode(controller, controller.selection.start).offset; controller.replaceText( offset, 1, block, TextSelection.collapsed(offset: offset)); } else { diff --git a/doc_cn.md b/doc_cn.md index 4eef88d0..d76f4c2e 100644 --- a/doc_cn.md +++ b/doc_cn.md @@ -328,7 +328,7 @@ Future _addEditNote(BuildContext context, {Document? document}) async { final length = controller.selection.extentOffset - index; if (isEditing) { - final offset = getEmbedNode(controller, controller.selection.start).item1; + final offset = getEmbedNode(controller, controller.selection.start).offset; controller.replaceText( offset, 1, block, TextSelection.collapsed(offset: offset)); } else { diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 7b3c57ba..6ed01d74 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -12,7 +12,6 @@ import 'package:flutter_quill/flutter_quill.dart' hide Text; import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:tuple/tuple.dart'; import '../universal_ui/universal_ui.dart'; import 'read_only_page.dart'; @@ -184,8 +183,8 @@ class _HomePageState extends State { height: 1.15, fontWeight: FontWeight.w300, ), - const Tuple2(16, 0), - const Tuple2(0, 0), + const VerticalSpacing(16, 0), + const VerticalSpacing(0, 0), null), sizeSmall: const TextStyle(fontSize: 9), ), @@ -219,8 +218,8 @@ class _HomePageState extends State { height: 1.15, fontWeight: FontWeight.w300, ), - const Tuple2(16, 0), - const Tuple2(0, 0), + const VerticalSpacing(16, 0), + const VerticalSpacing(0, 0), null), sizeSmall: const TextStyle(fontSize: 9), ), @@ -476,7 +475,8 @@ class _HomePageState extends State { final length = controller.selection.extentOffset - index; if (isEditing) { - final offset = getEmbedNode(controller, controller.selection.start).item1; + final offset = + getEmbedNode(controller, controller.selection.start).offset; controller.replaceText( offset, 1, block, TextSelection.collapsed(offset: offset)); } else { diff --git a/flutter_quill_extensions/lib/embeds/builders.dart b/flutter_quill_extensions/lib/embeds/builders.dart index c443cc59..4e7ebcbc 100644 --- a/flutter_quill_extensions/lib/embeds/builders.dart +++ b/flutter_quill_extensions/lib/embeds/builders.dart @@ -7,7 +7,6 @@ import 'package:flutter_quill/flutter_quill.dart' hide Text; import 'package:flutter_quill/translations.dart'; import 'package:gallery_saver/gallery_saver.dart'; import 'package:math_keyboard/math_keyboard.dart'; -import 'package:tuple/tuple.dart'; import 'utils.dart'; import 'widgets/image.dart'; @@ -30,7 +29,7 @@ class ImageEmbedBuilder implements EmbedBuilder { var image; final imageUrl = standardizeImageUrl(node.value.data); - Tuple2? _widthHeight; + OptionalSize? _imageSize; final style = node.style.attributes['style']; if (base.isMobile() && style != null) { final _attrs = base.parseKeyValuePairs(style.value.toString(), { @@ -46,7 +45,7 @@ class ImageEmbedBuilder implements EmbedBuilder { 'mobileWidth and mobileHeight must be specified'); final w = double.parse(_attrs[Attribute.mobileWidth]!); final h = double.parse(_attrs[Attribute.mobileHeight]!); - _widthHeight = Tuple2(w, h); + _imageSize = OptionalSize(w, h); final m = _attrs[Attribute.mobileMargin] == null ? 0.0 : double.parse(_attrs[Attribute.mobileMargin]!); @@ -57,9 +56,9 @@ class ImageEmbedBuilder implements EmbedBuilder { } } - if (_widthHeight == null) { + if (_imageSize == null) { image = imageByUrl(imageUrl); - _widthHeight = Tuple2((image as Image).width, image.height); + _imageSize = OptionalSize((image as Image).width, image.height); } if (!readOnly && base.isMobile()) { @@ -87,10 +86,10 @@ class ImageEmbedBuilder implements EmbedBuilder { controller ..skipRequestKeyboard = true ..formatText( - res.item1, 1, StyleAttribute(attr)); + res.offset, 1, StyleAttribute(attr)); }, - imageWidth: _widthHeight?.item1, - imageHeight: _widthHeight?.item2, + imageWidth: _imageSize?.width, + imageHeight: _imageSize?.height, maxWidth: _screenSize.width, maxHeight: _screenSize.height); }); @@ -103,10 +102,10 @@ class ImageEmbedBuilder implements EmbedBuilder { onPressed: () { final imageNode = getEmbedNode(controller, controller.selection.start) - .item2; + .value; final imageUrl = imageNode.value.data; controller.copiedImageUrl = - Tuple2(imageUrl, getImageStyleString(controller)); + ImageUrl(imageUrl, getImageStyleString(controller)); Navigator.pop(context); }, ); @@ -117,7 +116,7 @@ class ImageEmbedBuilder implements EmbedBuilder { onPressed: () { final offset = getEmbedNode(controller, controller.selection.start) - .item1; + .offset; controller.replaceText(offset, 1, '', TextSelection.collapsed(offset: offset)); Navigator.pop(context); diff --git a/flutter_quill_extensions/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index 4aef2ba6..5abc6d38 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -12,19 +12,19 @@ dependencies: flutter: sdk: flutter - flutter_quill: ^6.0.0 + flutter_quill: ^7.0.0 image_picker: ^0.8.5+3 photo_view: ^0.14.0 video_player: ^2.4.2 youtube_player_flutter: ^8.1.1 gallery_saver: ^2.3.2 - math_keyboard: ^0.1.6 - string_validator: ^0.3.0 + math_keyboard: ^0.1.8 + string_validator: ^1.0.0 -#dependency_overrides: -# flutter_quill: -# path: ../ +# dependency_overrides: +# flutter_quill: +# path: ../ dev_dependencies: flutter_test: diff --git a/lib/flutter_quill.dart b/lib/flutter_quill.dart index e74a41ac..aefc566c 100644 --- a/lib/flutter_quill.dart +++ b/lib/flutter_quill.dart @@ -9,6 +9,11 @@ export 'src/models/documents/nodes/line.dart'; export 'src/models/documents/nodes/node.dart'; export 'src/models/documents/style.dart'; export 'src/models/quill_delta.dart'; +export 'src/models/structs/doc_change.dart'; +export 'src/models/structs/image_url.dart'; +export 'src/models/structs/offset_value.dart'; +export 'src/models/structs/optional_size.dart'; +export 'src/models/structs/vertical_spacing.dart'; export 'src/models/themes/quill_custom_button.dart'; export 'src/models/themes/quill_dialog_theme.dart'; export 'src/models/themes/quill_icon_theme.dart'; diff --git a/lib/src/models/documents/document.dart b/lib/src/models/documents/document.dart index e3e2237f..1b4d5aef 100644 --- a/lib/src/models/documents/document.dart +++ b/lib/src/models/documents/document.dart @@ -1,9 +1,11 @@ import 'dart:async'; -import 'package:tuple/tuple.dart'; - import '../quill_delta.dart'; import '../rules/rule.dart'; +import '../structs/doc_change.dart'; +import '../structs/history_changed.dart'; +import '../structs/offset_value.dart'; +import '../structs/segment_leaf_node.dart'; import 'attribute.dart'; import 'history.dart'; import 'nodes/block.dart'; @@ -50,13 +52,12 @@ class Document { _rules.setCustomRules(customRules); } - final StreamController> _observer = - StreamController.broadcast(); + final StreamController _observer = StreamController.broadcast(); final History _history = History(); - /// Stream of [Change]s applied to this document. - Stream> get changes => _observer.stream; + /// Stream of [DocChange]s applied to this document. + Stream get changes => _observer.stream; /// Inserts [data] in this document at specified [index]. /// @@ -158,7 +159,7 @@ class Document { } /// Returns all styles for each node within selection - List> collectAllIndividualStyles(int index, int len) { + List> collectAllIndividualStyles(int index, int len) { final res = queryChild(index); return (res.node as Line).collectAllIndividualStyles(res.offset, len); } @@ -216,19 +217,15 @@ class Document { } /// Given offset, find its leaf node in document - Tuple2 querySegmentLeafNode(int offset) { + SegmentLeafNode querySegmentLeafNode(int offset) { final result = queryChild(offset); if (result.node == null) { - return const Tuple2(null, null); + return const SegmentLeafNode(null, null); } final line = result.node as Line; final segmentResult = line.queryChild(result.offset, false); - if (segmentResult.node == null) { - return Tuple2(line, null); - } - final segment = segmentResult.node as Leaf; - return Tuple2(line, segment); + return SegmentLeafNode(line, segmentResult.node as Leaf?); } /// Composes [change] Delta into this document. @@ -276,16 +273,16 @@ class Document { if (_delta != _root.toDelta()) { throw 'Compose failed'; } - final change = Tuple3(originalDelta, delta, changeSource); + final change = DocChange(originalDelta, delta, changeSource); _observer.add(change); _history.handleDocChange(change); } - Tuple2 undo() { + HistoryChanged undo() { return _history.undo(this); } - Tuple2 redo() { + HistoryChanged redo() { return _history.redo(this); } diff --git a/lib/src/models/documents/history.dart b/lib/src/models/documents/history.dart index d406505e..fa87700c 100644 --- a/lib/src/models/documents/history.dart +++ b/lib/src/models/documents/history.dart @@ -1,6 +1,6 @@ -import 'package:tuple/tuple.dart'; - import '../quill_delta.dart'; +import '../structs/doc_change.dart'; +import '../structs/history_changed.dart'; import 'document.dart'; class History { @@ -32,12 +32,12 @@ class History { ///record delay final int interval; - void handleDocChange(Tuple3 change) { + void handleDocChange(DocChange docChange) { if (ignoreChange) return; - if (!userOnly || change.item3 == ChangeSource.LOCAL) { - record(change.item2, change.item1); + if (!userOnly || docChange.source == ChangeSource.LOCAL) { + record(docChange.change, docChange.before); } else { - transform(change.item2); + transform(docChange.change); } } @@ -85,9 +85,9 @@ class History { } } - Tuple2 _change(Document doc, List source, List dest) { + HistoryChanged _change(Document doc, List source, List dest) { if (source.isEmpty) { - return const Tuple2(false, 0); + return const HistoryChanged(false, 0); } final delta = source.removeLast(); // look for insert or delete @@ -107,14 +107,14 @@ class History { ignoreChange = true; doc.compose(delta, ChangeSource.LOCAL); ignoreChange = false; - return Tuple2(true, len); + return HistoryChanged(true, len); } - Tuple2 undo(Document doc) { + HistoryChanged undo(Document doc) { return _change(doc, stack.undo, stack.redo); } - Tuple2 redo(Document doc) { + HistoryChanged redo(Document doc) { return _change(doc, stack.redo, stack.undo); } } diff --git a/lib/src/models/documents/nodes/line.dart b/lib/src/models/documents/nodes/line.dart index 4b6db80f..a8cca75c 100644 --- a/lib/src/models/documents/nodes/line.dart +++ b/lib/src/models/documents/nodes/line.dart @@ -1,9 +1,9 @@ import 'dart:math' as math; import 'package:collection/collection.dart'; -import 'package:tuple/tuple.dart'; import '../../quill_delta.dart'; +import '../../structs/offset_value.dart'; import '../attribute.dart'; import '../style.dart'; import 'block.dart'; @@ -391,10 +391,10 @@ class Line extends Container { /// Returns each node segment's offset in selection /// with its corresponding style as a list - List> collectAllIndividualStyles(int offset, int len, + List> collectAllIndividualStyles(int offset, int len, {int beg = 0}) { final local = math.min(length - offset, len); - final result = >[]; + final result = >[]; final data = queryChild(offset, true); var node = data.node as Leaf?; @@ -402,12 +402,12 @@ class Line extends Container { var pos = 0; if (node is Text) { pos = node.length - data.offset; - result.add(Tuple2(beg, node.style)); + result.add(OffsetValue(beg, node.style)); } while (!node!.isLast && pos < local) { node = node.next as Leaf; if (node is Text) { - result.add(Tuple2(pos + beg, node.style)); + result.add(OffsetValue(pos + beg, node.style)); pos += node.length; } } diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart index 949371b5..93cf4742 100644 --- a/lib/src/models/rules/insert.dart +++ b/lib/src/models/rules/insert.dart @@ -1,5 +1,3 @@ -import 'package:tuple/tuple.dart'; - import '../../models/documents/document.dart'; import '../documents/attribute.dart'; import '../documents/nodes/embeddable.dart'; @@ -56,7 +54,7 @@ class PreserveLineStyleOnSplitRule extends InsertRule { return delta; } final nextNewLine = _getNextNewLine(itr); - final attributes = nextNewLine.item1?.attributes; + final attributes = nextNewLine.operation?.attributes; return delta..insert('\n', attributes); } @@ -86,7 +84,8 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { // Look for the next newline. final nextNewLine = _getNextNewLine(itr); final lineStyle = - Style.fromJson(nextNewLine.item1?.attributes ?? {}); + Style.fromJson( + nextNewLine.operation?.attributes ?? {}); final blockStyle = lineStyle.getBlocksExceptHeader(); // Are we currently in a block? If not then ignore. @@ -126,8 +125,8 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { // Reset style of the original newline character if needed. if (resetStyle.isNotEmpty) { delta - ..retain(nextNewLine.item2!) - ..retain((nextNewLine.item1!.data as String).indexOf('\n')) + ..retain(nextNewLine.skipped!) + ..retain((nextNewLine.operation!.data as String).indexOf('\n')) ..retain(1, resetStyle); } @@ -188,10 +187,10 @@ class AutoExitBlockRule extends InsertRule { // Keep looking for the next newline character to see if it shares the same // block style as `cur`. final nextNewLine = _getNextNewLine(itr); - if (nextNewLine.item1 != null && - nextNewLine.item1!.attributes != null && - Style.fromJson(nextNewLine.item1!.attributes).getBlockExceptHeader() == - blockStyle) { + if (nextNewLine.operation != null && + nextNewLine.operation!.attributes != null && + Style.fromJson(nextNewLine.operation!.attributes).getBlockExceptHeader() + == blockStyle) { // We are not at the end of this block, ignore. return null; } @@ -524,15 +523,22 @@ class CatchAllInsertRule extends InsertRule { } } -Tuple2 _getNextNewLine(DeltaIterator iterator) { +_NextNewLine _getNextNewLine(DeltaIterator iterator) { Operation op; for (var skipped = 0; iterator.hasNext; skipped += op.length!) { op = iterator.next(); final lineBreak = (op.data is String ? op.data as String? : '')!.indexOf('\n'); if (lineBreak >= 0) { - return Tuple2(op, skipped); + return _NextNewLine(op, skipped); } } - return const Tuple2(null, null); + return const _NextNewLine(null, null); +} + +class _NextNewLine { + const _NextNewLine(this.operation, this.skipped); + + final Operation? operation; + final int? skipped; } diff --git a/lib/src/models/structs/doc_change.dart b/lib/src/models/structs/doc_change.dart new file mode 100644 index 00000000..6c999331 --- /dev/null +++ b/lib/src/models/structs/doc_change.dart @@ -0,0 +1,18 @@ +import '../../../flutter_quill.dart'; + +class DocChange { + DocChange( + this.before, + this.change, + this.source, + ); + + /// Document state before [change]. + final Delta before; + + /// Change delta applied to the document. + final Delta change; + + /// The source of this change. + final ChangeSource source; +} diff --git a/lib/src/models/structs/history_changed.dart b/lib/src/models/structs/history_changed.dart new file mode 100644 index 00000000..abb61567 --- /dev/null +++ b/lib/src/models/structs/history_changed.dart @@ -0,0 +1,9 @@ +class HistoryChanged { + const HistoryChanged( + this.changed, + this.len, + ); + + final bool changed; + final int? len; +} diff --git a/lib/src/models/structs/image_url.dart b/lib/src/models/structs/image_url.dart new file mode 100644 index 00000000..097e199b --- /dev/null +++ b/lib/src/models/structs/image_url.dart @@ -0,0 +1,9 @@ +class ImageUrl { + const ImageUrl( + this.url, + this.styleString, + ); + + final String url; + final String styleString; +} diff --git a/lib/src/models/structs/offset_value.dart b/lib/src/models/structs/offset_value.dart new file mode 100644 index 00000000..0f9e558b --- /dev/null +++ b/lib/src/models/structs/offset_value.dart @@ -0,0 +1,5 @@ +class OffsetValue { + OffsetValue(this.offset, this.value); + final int offset; + final T value; +} diff --git a/lib/src/models/structs/optional_size.dart b/lib/src/models/structs/optional_size.dart new file mode 100644 index 00000000..a887b468 --- /dev/null +++ b/lib/src/models/structs/optional_size.dart @@ -0,0 +1,14 @@ +class OptionalSize { + OptionalSize( + this.width, + this.height, + ); + + /// If non-null, requires the child to have exactly this width. + /// If null, the child is free to choose its own width. + final double? width; + + /// If non-null, requires the child to have exactly this height. + /// If null, the child is free to choose its own height. + final double? height; +} diff --git a/lib/src/models/structs/segment_leaf_node.dart b/lib/src/models/structs/segment_leaf_node.dart new file mode 100644 index 00000000..676f037f --- /dev/null +++ b/lib/src/models/structs/segment_leaf_node.dart @@ -0,0 +1,8 @@ +import '../../../flutter_quill.dart'; + +class SegmentLeafNode { + const SegmentLeafNode(this.line, this.leaf); + + final Line? line; + final Leaf? leaf; +} diff --git a/lib/src/models/structs/vertical_spacing.dart b/lib/src/models/structs/vertical_spacing.dart new file mode 100644 index 00000000..54f76f7c --- /dev/null +++ b/lib/src/models/structs/vertical_spacing.dart @@ -0,0 +1,9 @@ +class VerticalSpacing { + const VerticalSpacing( + this.top, + this.bottom, + ); + + final double top; + final double bottom; +} diff --git a/lib/src/utils/embeds.dart b/lib/src/utils/embeds.dart index 7ee2dd0d..db693ba8 100644 --- a/lib/src/utils/embeds.dart +++ b/lib/src/utils/embeds.dart @@ -1,11 +1,10 @@ import 'dart:math'; -import 'package:tuple/tuple.dart'; - import '../models/documents/nodes/leaf.dart'; +import '../models/structs/offset_value.dart'; import '../widgets/controller.dart'; -Tuple2 getEmbedNode(QuillController controller, int offset) { +OffsetValue getEmbedNode(QuillController controller, int offset) { var offset = controller.selection.start; var embedNode = controller.queryNode(offset); if (embedNode == null || !(embedNode is Embed)) { @@ -13,7 +12,7 @@ Tuple2 getEmbedNode(QuillController controller, int offset) { embedNode = controller.queryNode(offset); } if (embedNode != null && embedNode is Embed) { - return Tuple2(offset, embedNode); + return OffsetValue(offset, embedNode); } return throw 'Embed node not found by offset $offset'; diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index fd85666b..30396377 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -2,7 +2,6 @@ import 'dart:math' as math; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; -import 'package:tuple/tuple.dart'; import '../models/documents/attribute.dart'; import '../models/documents/document.dart'; @@ -10,6 +9,9 @@ import '../models/documents/nodes/embeddable.dart'; import '../models/documents/nodes/leaf.dart'; import '../models/documents/style.dart'; import '../models/quill_delta.dart'; +import '../models/structs/doc_change.dart'; +import '../models/structs/image_url.dart'; +import '../models/structs/offset_value.dart'; import '../utils/delta.dart'; typedef ReplaceTextCallback = bool Function(int index, int len, Object? data); @@ -82,12 +84,7 @@ class QuillController extends ChangeNotifier { /// removing or listeners to this instance. bool _isDisposed = false; - // item1: Document state before [change]. - // - // item2: Change delta applied to the document. - // - // item3: The source of this change. - Stream> get changes => document.changes; + Stream get changes => document.changes; TextEditingValue get plainTextEditingValue => TextEditingValue( text: document.toPlainText(), @@ -123,7 +120,7 @@ class QuillController extends ChangeNotifier { } /// Returns all styles for each node within selection - List> getAllIndividualSelectionStyles() { + List> getAllIndividualSelectionStyles() { final styles = document.collectAllIndividualStyles( selection.start, selection.end - selection.start); return styles; @@ -145,9 +142,9 @@ class QuillController extends ChangeNotifier { } void undo() { - final tup = document.undo(); - if (tup.item1) { - _handleHistoryChange(tup.item2); + final result = document.undo(); + if (result.changed) { + _handleHistoryChange(result.len); } } @@ -167,9 +164,9 @@ class QuillController extends ChangeNotifier { } void redo() { - final tup = document.redo(); - if (tup.item1) { - _handleHistoryChange(tup.item2); + final result = document.redo(); + if (result.changed) { + _handleHistoryChange(result.len); } } @@ -369,16 +366,15 @@ class QuillController extends ChangeNotifier { /// Given offset, find its leaf node in document Leaf? queryNode(int offset) { - return document.querySegmentLeafNode(offset).item2; + return document.querySegmentLeafNode(offset).leaf; } /// Clipboard for image url and its corresponding style - /// item1 is url and item2 is style string - Tuple2? _copiedImageUrl; + ImageUrl? _copiedImageUrl; - Tuple2? get copiedImageUrl => _copiedImageUrl; + ImageUrl? get copiedImageUrl => _copiedImageUrl; - set copiedImageUrl(Tuple2? value) { + set copiedImageUrl(ImageUrl? value) { _copiedImageUrl = value; Clipboard.setData(const ClipboardData(text: '')); } diff --git a/lib/src/widgets/default_styles.dart b/lib/src/widgets/default_styles.dart index 6c766e2b..08acb154 100644 --- a/lib/src/widgets/default_styles.dart +++ b/lib/src/widgets/default_styles.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:tuple/tuple.dart'; import '../models/documents/attribute.dart'; import '../models/documents/style.dart'; +import '../models/structs/vertical_spacing.dart'; import '../utils/platform.dart'; import 'style_widgets/checkbox_point.dart'; @@ -44,11 +44,11 @@ class DefaultTextBlockStyle { final TextStyle style; /// Vertical spacing around a text block. - final Tuple2 verticalSpacing; + final VerticalSpacing verticalSpacing; /// Vertical spacing for individual lines within a text block. /// - final Tuple2 lineSpacing; + final VerticalSpacing lineSpacing; /// Decoration of a text block. /// @@ -125,8 +125,8 @@ class InlineCodeStyle { class DefaultListBlockStyle extends DefaultTextBlockStyle { DefaultListBlockStyle( TextStyle style, - Tuple2 verticalSpacing, - Tuple2 lineSpacing, + VerticalSpacing verticalSpacing, + VerticalSpacing lineSpacing, BoxDecoration? decoration, this.checkboxUIBuilder, ) : super(style, verticalSpacing, lineSpacing, decoration); @@ -193,7 +193,7 @@ class DefaultStyles { height: 1.3, decoration: TextDecoration.none, ); - const baseSpacing = Tuple2(6, 0); + const baseSpacing = VerticalSpacing(6, 0); String fontFamily; if (isAppleOS(themeData.platform)) { fontFamily = 'Menlo'; @@ -216,8 +216,8 @@ class DefaultStyles { fontWeight: FontWeight.w300, decoration: TextDecoration.none, ), - const Tuple2(16, 0), - const Tuple2(0, 0), + const VerticalSpacing(16, 0), + const VerticalSpacing(0, 0), null), h2: DefaultTextBlockStyle( defaultTextStyle.style.copyWith( @@ -227,8 +227,8 @@ class DefaultStyles { fontWeight: FontWeight.normal, decoration: TextDecoration.none, ), - const Tuple2(8, 0), - const Tuple2(0, 0), + const VerticalSpacing(8, 0), + const VerticalSpacing(0, 0), null), h3: DefaultTextBlockStyle( defaultTextStyle.style.copyWith( @@ -238,11 +238,14 @@ class DefaultStyles { fontWeight: FontWeight.w500, decoration: TextDecoration.none, ), - const Tuple2(8, 0), - const Tuple2(0, 0), + const VerticalSpacing(8, 0), + const VerticalSpacing(0, 0), null), paragraph: DefaultTextBlockStyle( - baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null), + baseStyle, + const VerticalSpacing(0, 0), + const VerticalSpacing(0, 0), + null), bold: const TextStyle(fontWeight: FontWeight.bold), italic: const TextStyle(fontStyle: FontStyle.italic), small: const TextStyle(fontSize: 12), @@ -272,15 +275,15 @@ class DefaultStyles { height: 1.5, color: Colors.grey.withOpacity(0.6), ), - const Tuple2(0, 0), - const Tuple2(0, 0), + const VerticalSpacing(0, 0), + const VerticalSpacing(0, 0), null), lists: DefaultListBlockStyle( - baseStyle, baseSpacing, const Tuple2(0, 6), null, null), + baseStyle, baseSpacing, const VerticalSpacing(0, 6), null, null), quote: DefaultTextBlockStyle( TextStyle(color: baseStyle.color!.withOpacity(0.6)), baseSpacing, - const Tuple2(6, 2), + const VerticalSpacing(6, 2), BoxDecoration( border: Border( left: BorderSide(width: 4, color: Colors.grey.shade300), @@ -294,17 +297,23 @@ class DefaultStyles { height: 1.15, ), baseSpacing, - const Tuple2(0, 0), + const VerticalSpacing(0, 0), BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(2), )), indent: DefaultTextBlockStyle( - baseStyle, baseSpacing, const Tuple2(0, 6), null), + baseStyle, baseSpacing, const VerticalSpacing(0, 6), null), align: DefaultTextBlockStyle( - baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null), + baseStyle, + const VerticalSpacing(0, 0), + const VerticalSpacing(0, 0), + null), leading: DefaultTextBlockStyle( - baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null), + baseStyle, + const VerticalSpacing(0, 0), + const VerticalSpacing(0, 0), + null), sizeSmall: const TextStyle(fontSize: 10), sizeLarge: const TextStyle(fontSize: 18), sizeHuge: const TextStyle(fontSize: 22)); diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 7c8d7c10..7246a881 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -9,13 +9,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:i18n_extension/i18n_widget.dart'; -import 'package:tuple/tuple.dart'; import '../models/documents/document.dart'; import '../models/documents/nodes/container.dart' as container_node; import '../models/documents/nodes/embeddable.dart'; import '../models/documents/nodes/leaf.dart'; import '../models/documents/style.dart'; +import '../models/structs/offset_value.dart'; import '../utils/platform.dart'; import 'box.dart'; import 'controller.dart'; @@ -38,7 +38,7 @@ abstract class EditorState extends State EditorTextSelectionOverlay? get selectionOverlay; - List> get pasteStyle; + List> get pasteStyle; String get pastePlainText; @@ -645,11 +645,11 @@ class _QuillEditorSelectionGestureDetectorBuilder final pos = renderEditor!.getPositionForOffset(details.globalPosition); final result = editor!.widget.controller.document.querySegmentLeafNode(pos.offset); - final line = result.item1; + final line = result.line; if (line == null) { return false; } - final segmentLeaf = result.item2; + final segmentLeaf = result.leaf; if (segmentLeaf == null && line.length == 1) { editor!.widget.controller.updateSelection( TextSelection.collapsed(offset: pos.offset), ChangeSource.LOCAL); diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 75a4203c..f78b18c3 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -12,8 +12,8 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:pasteboard/pasteboard.dart'; -import 'package:tuple/tuple.dart'; +import '../../flutter_quill.dart'; import '../models/documents/attribute.dart'; import '../models/documents/document.dart'; import '../models/documents/nodes/block.dart'; @@ -22,6 +22,7 @@ import '../models/documents/nodes/leaf.dart' as leaf; import '../models/documents/nodes/line.dart'; import '../models/documents/nodes/node.dart'; import '../models/documents/style.dart'; +import '../models/structs/vertical_spacing.dart'; import '../utils/cast.dart'; import '../utils/delta.dart'; import '../utils/embeds.dart'; @@ -288,8 +289,8 @@ class RawEditorState extends EditorState // for pasting style @override - List> get pasteStyle => _pasteStyle; - List> _pasteStyle = >[]; + List> get pasteStyle => _pasteStyle; + List> _pasteStyle = >[]; @override String get pastePlainText => _pastePlainText; @@ -331,8 +332,8 @@ class RawEditorState extends EditorState final points = renderEditor.getEndpointsForSelection(selection); return TextSelectionToolbarAnchors.fromSelection( renderBox: renderEditor, - startGlyphHeight: glyphHeights.item1, - endGlyphHeight: glyphHeights.item2, + startGlyphHeight: glyphHeights.startGlyphHeight, + endGlyphHeight: glyphHeights.endGlyphHeight, selectionEndpoints: points, ); } @@ -341,7 +342,7 @@ class RawEditorState extends EditorState /// [RawEditorState]. /// /// Copied from [EditableTextState]. - Tuple2 _getGlyphHeights() { + _GlyphHeights _getGlyphHeights() { final selection = textEditingValue.selection; // Only calculate handle rects if the text in the previous frame @@ -354,7 +355,7 @@ class RawEditorState extends EditorState final prevText = renderEditor.document.toPlainText(); final currText = textEditingValue.text; if (prevText != currText || !selection.isValid || selection.isCollapsed) { - return Tuple2( + return _GlyphHeights( renderEditor.preferredLineHeight(selection.base), renderEditor.preferredLineHeight(selection.base), ); @@ -364,7 +365,7 @@ class RawEditorState extends EditorState renderEditor.getLocalRectForCaret(selection.base); final endCharacterRect = renderEditor.getLocalRectForCaret(selection.extent); - return Tuple2( + return _GlyphHeights( startCharacterRect.height, endCharacterRect.height, ); @@ -414,7 +415,7 @@ class RawEditorState extends EditorState /// baseline. // This implies that the first line has no styles applied to it. final baselinePadding = - EdgeInsets.only(top: _styles!.paragraph!.verticalSpacing.item1); + EdgeInsets.only(top: _styles!.paragraph!.verticalSpacing.top); child = BaselineProxy( textStyle: _styles!.paragraph!.style, padding: baselinePadding, @@ -778,7 +779,7 @@ class RawEditorState extends EditorState return editableTextLine; } - Tuple2 _getVerticalSpacingForLine( + VerticalSpacing _getVerticalSpacingForLine( Line line, DefaultStyles? defaultStyles) { final attrs = line.style.attributes; if (attrs.containsKey(Attribute.header.key)) { @@ -803,7 +804,7 @@ class RawEditorState extends EditorState return defaultStyles!.paragraph!.verticalSpacing; } - Tuple2 _getVerticalSpacingForBlock( + VerticalSpacing _getVerticalSpacingForBlock( Block node, DefaultStyles? defaultStyles) { final attrs = node.style.attributes; if (attrs.containsKey(Attribute.blockQuote.key)) { @@ -817,7 +818,7 @@ class RawEditorState extends EditorState } else if (attrs.containsKey(Attribute.align.key)) { return defaultStyles!.align!.verticalSpacing; } - return const Tuple2(0, 0); + return const VerticalSpacing(0, 0); } @override @@ -1281,10 +1282,10 @@ class RawEditorState extends EditorState final length = textEditingValue.selection.extentOffset - index; final copied = controller.copiedImageUrl!; controller.replaceText( - index, length, BlockEmbed.image(copied.item1), null); - if (copied.item2.isNotEmpty) { - controller.formatText(getEmbedNode(controller, index + 1).item1, 1, - StyleAttribute(copied.item2)); + index, length, BlockEmbed.image(copied.url), null); + if (copied.styleString.isNotEmpty) { + controller.formatText(getEmbedNode(controller, index + 1).offset, 1, + StyleAttribute(copied.styleString)); } controller.copiedImageUrl = null; await Clipboard.setData(const ClipboardData(text: '')); @@ -2427,3 +2428,13 @@ typedef QuillEditorContextMenuBuilder = Widget Function( BuildContext context, RawEditorState rawEditorState, ); + +class _GlyphHeights { + _GlyphHeights( + this.startGlyphHeight, + this.endGlyphHeight, + ); + + final double startGlyphHeight; + final double endGlyphHeight; +} diff --git a/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart index 5da7cef2..fdd9244b 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart @@ -38,13 +38,13 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState if (insertedText == pastePlainText && pastePlainText != '') { final pos = start; for (var i = 0; i < pasteStyle.length; i++) { - final offset = pasteStyle[i].item1; - final style = pasteStyle[i].item2; + final offset = pasteStyle[i].offset; + final style = pasteStyle[i].value; widget.controller.formatTextStyle( pos + offset, i == pasteStyle.length - 1 ? pastePlainText.length - offset - : pasteStyle[i + 1].item1, + : pasteStyle[i + 1].offset, style); } } diff --git a/lib/src/widgets/text_block.dart b/lib/src/widgets/text_block.dart index f30e71cc..8ece7cf5 100644 --- a/lib/src/widgets/text_block.dart +++ b/lib/src/widgets/text_block.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:tuple/tuple.dart'; import '../models/documents/attribute.dart'; import '../models/documents/nodes/block.dart'; import '../models/documents/nodes/line.dart'; +import '../models/structs/vertical_spacing.dart'; import '../utils/delta.dart'; import 'box.dart'; import 'controller.dart'; @@ -78,7 +78,7 @@ class EditableTextBlock extends StatelessWidget { final QuillController controller; final TextDirection textDirection; final double scrollBottomInset; - final Tuple2 verticalSpacing; + final VerticalSpacing verticalSpacing; final TextSelection textSelection; final Color color; final DefaultStyles? styles; @@ -102,7 +102,7 @@ class EditableTextBlock extends StatelessWidget { return _EditableBlock( block: block, textDirection: textDirection, - padding: verticalSpacing as Tuple2, + padding: verticalSpacing, scrollBottomInset: scrollBottomInset, decoration: _getDecorationForBlock(block, defaultStyles) ?? const BoxDecoration(), @@ -243,7 +243,7 @@ class EditableTextBlock extends StatelessWidget { return baseIndent + extraIndent; } - Tuple2 _getSpacingForLine( + VerticalSpacing _getSpacingForLine( Line node, int index, int count, DefaultStyles? defaultStyles) { var top = 0.0, bottom = 0.0; @@ -252,22 +252,22 @@ class EditableTextBlock extends StatelessWidget { final level = attrs[Attribute.header.key]!.value; switch (level) { case 1: - top = defaultStyles!.h1!.verticalSpacing.item1; - bottom = defaultStyles.h1!.verticalSpacing.item2; + top = defaultStyles!.h1!.verticalSpacing.top; + bottom = defaultStyles.h1!.verticalSpacing.bottom; break; case 2: - top = defaultStyles!.h2!.verticalSpacing.item1; - bottom = defaultStyles.h2!.verticalSpacing.item2; + top = defaultStyles!.h2!.verticalSpacing.top; + bottom = defaultStyles.h2!.verticalSpacing.bottom; break; case 3: - top = defaultStyles!.h3!.verticalSpacing.item1; - bottom = defaultStyles.h3!.verticalSpacing.item2; + top = defaultStyles!.h3!.verticalSpacing.top; + bottom = defaultStyles.h3!.verticalSpacing.bottom; break; default: throw 'Invalid level $level'; } } else { - late Tuple2 lineSpacing; + late VerticalSpacing lineSpacing; if (attrs.containsKey(Attribute.blockQuote.key)) { lineSpacing = defaultStyles!.quote!.lineSpacing; } else if (attrs.containsKey(Attribute.indent.key)) { @@ -282,8 +282,8 @@ class EditableTextBlock extends StatelessWidget { // use paragraph linespacing as a default lineSpacing = defaultStyles!.paragraph!.lineSpacing; } - top = lineSpacing.item1; - bottom = lineSpacing.item2; + top = lineSpacing.top; + bottom = lineSpacing.bottom; } if (index == 1) { @@ -294,7 +294,7 @@ class EditableTextBlock extends StatelessWidget { bottom = 0.0; } - return Tuple2(top, bottom); + return VerticalSpacing(top, bottom); } } @@ -601,13 +601,13 @@ class _EditableBlock extends MultiChildRenderObjectWidget { final Block block; final TextDirection textDirection; - final Tuple2 padding; + final VerticalSpacing padding; final double scrollBottomInset; final Decoration decoration; final EdgeInsets? contentPadding; EdgeInsets get _padding => - EdgeInsets.only(top: padding.item1, bottom: padding.item2); + EdgeInsets.only(top: padding.top, bottom: padding.bottom); EdgeInsets get _contentPadding => contentPadding ?? EdgeInsets.zero; diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index b20ea22f..0f2995bb 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -6,7 +6,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; -import 'package:tuple/tuple.dart'; import 'package:url_launcher/url_launcher.dart'; import '../models/documents/attribute.dart'; @@ -16,6 +15,7 @@ import '../models/documents/nodes/leaf.dart' as leaf; import '../models/documents/nodes/line.dart'; import '../models/documents/nodes/node.dart'; import '../models/documents/style.dart'; +import '../models/structs/vertical_spacing.dart'; import '../utils/color.dart'; import '../utils/font.dart'; import '../utils/platform.dart'; @@ -476,7 +476,7 @@ class EditableTextLine extends RenderObjectWidget { final Widget? leading; final Widget body; final double indentWidth; - final Tuple2 verticalSpacing; + final VerticalSpacing verticalSpacing; final TextDirection textDirection; final TextSelection textSelection; final Color color; @@ -526,8 +526,8 @@ class EditableTextLine extends RenderObjectWidget { EdgeInsetsGeometry _getPadding() { return EdgeInsetsDirectional.only( start: indentWidth, - top: verticalSpacing.item1, - bottom: verticalSpacing.item2); + top: verticalSpacing.top, + bottom: verticalSpacing.bottom); } } diff --git a/lib/src/widgets/toolbar/link_style_button.dart b/lib/src/widgets/toolbar/link_style_button.dart index fde466e4..80086608 100644 --- a/lib/src/widgets/toolbar/link_style_button.dart +++ b/lib/src/widgets/toolbar/link_style_button.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:tuple/tuple.dart'; import '../../models/documents/attribute.dart'; import '../../models/rules/insert.dart'; @@ -86,7 +85,7 @@ class _LinkStyleButtonState extends State { } void _openLinkDialog(BuildContext context) { - showDialog( + showDialog<_TextLink>( context: context, builder: (ctx) { final link = _getLinkAttributeValue(); @@ -96,7 +95,7 @@ class _LinkStyleButtonState extends State { if (link != null) { // text should be the link's corresponding text, not selection final leaf = - widget.controller.document.querySegmentLeafNode(index).item2; + widget.controller.document.querySegmentLeafNode(index).leaf; if (leaf != null) { text = leaf.toPlainText(); } @@ -122,24 +121,21 @@ class _LinkStyleButtonState extends State { ?.value; } - void _linkSubmitted(dynamic value) { - // text.isNotEmpty && link.isNotEmpty - final String text = (value as Tuple2).item1; - final String link = value.item2.trim(); - + void _linkSubmitted(_TextLink value) { var index = widget.controller.selection.start; var length = widget.controller.selection.end - index; if (_getLinkAttributeValue() != null) { // text should be the link's corresponding text, not selection - final leaf = widget.controller.document.querySegmentLeafNode(index).item2; + final leaf = widget.controller.document.querySegmentLeafNode(index).leaf; if (leaf != null) { final range = getLinkRange(leaf); index = range.start; length = range.end - range.start; } } - widget.controller.replaceText(index, length, text, null); - widget.controller.formatText(index, text.length, LinkAttribute(link)); + widget.controller.replaceText(index, length, value.text, null); + widget.controller.formatText( + index, value.text.length, LinkAttribute(value.link)); } } @@ -240,6 +236,16 @@ class _LinkDialogState extends State<_LinkDialog> { } void _applyLink() { - Navigator.pop(context, Tuple2(_text.trim(), _link.trim())); + Navigator.pop(context, _TextLink(_text.trim(), _link.trim())); } } + +class _TextLink { + _TextLink( + this.text, + this.link, + ); + + final String text; + final String link; +} diff --git a/pubspec.yaml b/pubspec.yaml index 06f8fa10..95b4454b 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: 6.4.4 +version: 7.0.0 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill @@ -16,12 +16,11 @@ dependencies: flutter_colorpicker: ^1.0.3 flutter_keyboard_visibility: ^5.4.0 quiver: ^3.2.1 - tuple: ^2.0.1 url_launcher: ^6.1.9 pedantic: ^1.11.1 characters: ^1.2.1 diff_match_patch: ^0.4.1 - i18n_extension: ^7.0.0 + i18n_extension: ^6.0.0 device_info_plus: ^8.1.0 platform: ^3.1.0 pasteboard: ^0.2.0