From bd109bffcc0a899dd98cdf3369a127cb2702f10c Mon Sep 17 00:00:00 2001 From: li3317 Date: Fri, 27 Aug 2021 22:13:41 -0400 Subject: [PATCH 001/179] Skip image when pasting --- lib/src/widgets/raw_editor.dart | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 1f350646..4f2ea0ad 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -637,10 +637,25 @@ class RawEditorState extends EditorState if (data != null) { final length = textEditingValue.selection.end - textEditingValue.selection.start; + var str = data.text!; + final codes = data.text!.codeUnits; + // For clip from editor, it may contain image, a.k.a 65532. + // For clip from browser, image is directly ignore. + // Here we skip image when pasting. + if (codes.contains(65532)) { + final sb = StringBuffer(); + for (var i = 0; i < str.length; i++) { + if (str.codeUnitAt(i) == 65532) { + continue; + } + sb.write(str[i]); + } + str = sb.toString(); + } widget.controller.replaceText( value.selection.start, length, - data.text, + str, value.selection, ); // move cursor to the end of pasted text selection From 36fb2e1ca8d46487719b5715a17e6ad83380bca6 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 27 Aug 2021 20:15:52 -0700 Subject: [PATCH 002/179] Upgrade to [1.9.5] Skip image when pasting. --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e86aea7f..ae59e44f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.9.5] +* Skip image when pasting. + ## [1.9.4] * Bug fix for cursor position when tapping at the end of line with image(s). diff --git a/pubspec.yaml b/pubspec.yaml index 9becfd1e..39bd4c97 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.9.4 +version: 1.9.5 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From a24241cf6bda8804dec6709ffaab6af1f0943e09 Mon Sep 17 00:00:00 2001 From: kevinDespoulains <46108869+kevinDespoulains@users.noreply.github.com> Date: Mon, 30 Aug 2021 16:05:17 +0200 Subject: [PATCH 003/179] Raw editor update to support putting QuillEditor inside a Scrollable view (#371) --- lib/src/widgets/raw_editor.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 4f2ea0ad..09b4940b 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -564,7 +564,7 @@ class RawEditorState extends EditorState _showCaretOnScreenScheduled = true; SchedulerBinding.instance!.addPostFrameCallback((_) { - if (widget.scrollable) { + if (widget.scrollable || _scrollController.hasClients) { _showCaretOnScreenScheduled = false; final renderEditor = getRenderEditor(); @@ -608,6 +608,7 @@ class RawEditorState extends EditorState void requestKeyboard() { if (_hasFocus) { openConnectionIfNeeded(); + _showCaretOnScreen(); } else { widget.focusNode.requestFocus(); } From a585a880cf9f7c3d65014dc52f8511676c420d94 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Mon, 30 Aug 2021 08:59:27 -0700 Subject: [PATCH 004/179] Upgrade to [1.9.6] Support putting QuillEditor inside a Scrollable view. --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae59e44f..427d4c95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.9.6] +* Support putting QuillEditor inside a Scrollable view. + ## [1.9.5] * Skip image when pasting. diff --git a/pubspec.yaml b/pubspec.yaml index 39bd4c97..4a442472 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.9.5 +version: 1.9.6 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 7c7f61fe90b45ebb35f99a308c289c417fbdcdb1 Mon Sep 17 00:00:00 2001 From: kevinDespoulains <46108869+kevinDespoulains@users.noreply.github.com> Date: Tue, 31 Aug 2021 18:10:08 +0200 Subject: [PATCH 005/179] Option added to controller to keep inline style on new lines (#372) Co-authored-by: Kevin Despoulains --- lib/src/widgets/controller.dart | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index f26765b0..7d2e0714 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -14,7 +14,9 @@ class QuillController extends ChangeNotifier { QuillController({ required this.document, required TextSelection selection, - }) : _selection = selection; + bool keepStyleOnNewLine = false, + }) : _selection = selection, + _keepStyleOnNewLine = keepStyleOnNewLine; factory QuillController.basic() { return QuillController( @@ -26,6 +28,10 @@ class QuillController extends ChangeNotifier { /// Document managed by this controller. final Document document; + /// Tells whether to keep or reset the [toggledStyle] + /// when user adds a new line. + final bool _keepStyleOnNewLine; + /// Currently selected text within the [document]. TextSelection get selection => _selection; TextSelection _selection; @@ -135,7 +141,14 @@ class QuillController extends ChangeNotifier { } } - toggledStyle = Style(); + if (_keepStyleOnNewLine) { + final style = getSelectionStyle(); + final notInlineStyle = style.attributes.values.where((s) => !s.isInline); + toggledStyle = style.removeAll(notInlineStyle.toSet()); + } else { + toggledStyle = Style(); + } + if (textSelection != null) { if (delta == null || delta.isEmpty) { _updateSelection(textSelection, ChangeSource.LOCAL); From 15bd6f2342bbc8b988a627c86f6bcf9d773ca848 Mon Sep 17 00:00:00 2001 From: Xun Gong Date: Wed, 1 Sep 2021 13:09:20 -0700 Subject: [PATCH 006/179] Add "small" (extended) markdown style support --- lib/src/models/documents/attribute.dart | 8 ++++++++ lib/src/widgets/default_styles.dart | 4 ++++ lib/src/widgets/text_line.dart | 1 + lib/src/widgets/toolbar.dart | 9 +++++++++ 4 files changed, 22 insertions(+) diff --git a/lib/src/models/documents/attribute.dart b/lib/src/models/documents/attribute.dart index 97d82f15..467742d5 100644 --- a/lib/src/models/documents/attribute.dart +++ b/lib/src/models/documents/attribute.dart @@ -19,6 +19,7 @@ class Attribute { static final Map _registry = LinkedHashMap.of({ Attribute.bold.key: Attribute.bold, Attribute.italic.key: Attribute.italic, + Attribute.small.key: Attribute.small, Attribute.underline.key: Attribute.underline, Attribute.strikeThrough.key: Attribute.strikeThrough, Attribute.font.key: Attribute.font, @@ -43,6 +44,8 @@ class Attribute { static final ItalicAttribute italic = ItalicAttribute(); + static final SmallAttribute small = SmallAttribute(); + static final UnderlineAttribute underline = UnderlineAttribute(); static final StrikeThroughAttribute strikeThrough = StrikeThroughAttribute(); @@ -82,6 +85,7 @@ class Attribute { static final Set inlineKeys = { Attribute.bold.key, Attribute.italic.key, + Attribute.small.key, Attribute.underline.key, Attribute.strikeThrough.key, Attribute.link.key, @@ -217,6 +221,10 @@ class ItalicAttribute extends Attribute { ItalicAttribute() : super('italic', AttributeScope.INLINE, true); } +class SmallAttribute extends Attribute { + SmallAttribute() : super('small', AttributeScope.INLINE, true); +} + class UnderlineAttribute extends Attribute { UnderlineAttribute() : super('underline', AttributeScope.INLINE, true); } diff --git a/lib/src/widgets/default_styles.dart b/lib/src/widgets/default_styles.dart index 1cebe135..ca00cf19 100644 --- a/lib/src/widgets/default_styles.dart +++ b/lib/src/widgets/default_styles.dart @@ -51,6 +51,7 @@ class DefaultStyles { this.paragraph, this.bold, this.italic, + this.small, this.underline, this.strikeThrough, this.link, @@ -73,6 +74,7 @@ class DefaultStyles { final DefaultTextBlockStyle? paragraph; final TextStyle? bold; final TextStyle? italic; + final TextStyle? small; final TextStyle? underline; final TextStyle? strikeThrough; final TextStyle? sizeSmall; // 'small' @@ -147,6 +149,7 @@ class DefaultStyles { baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null), bold: const TextStyle(fontWeight: FontWeight.bold), italic: const TextStyle(fontStyle: FontStyle.italic), + small: const TextStyle(fontSize: 12), underline: const TextStyle(decoration: TextDecoration.underline), strikeThrough: const TextStyle(decoration: TextDecoration.lineThrough), link: TextStyle( @@ -205,6 +208,7 @@ class DefaultStyles { paragraph: other.paragraph ?? paragraph, bold: other.bold ?? bold, italic: other.italic ?? italic, + small: other.small ?? small, underline: other.underline ?? underline, strikeThrough: other.strikeThrough ?? strikeThrough, link: other.link ?? link, diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index 44715f6e..6da21a4c 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -181,6 +181,7 @@ class TextLine extends StatelessWidget { { Attribute.bold.key: defaultStyles.bold, Attribute.italic.key: defaultStyles.italic, + Attribute.small.key: defaultStyles.small, Attribute.link.key: defaultStyles.link, Attribute.underline.key: defaultStyles.underline, Attribute.strikeThrough.key: defaultStyles.strikeThrough, diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index dff2013c..2803948d 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -65,6 +65,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { double toolbarIconSize = kDefaultIconSize, bool showBoldButton = true, bool showItalicButton = true, + bool showSmallButton = false, bool showUnderLineButton = true, bool showStrikeThrough = true, bool showColorButton = true, @@ -96,6 +97,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { showHistory || showBoldButton || showItalicButton || + showSmallButton || showUnderLineButton || showStrikeThrough || showColorButton || @@ -142,6 +144,13 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { iconSize: toolbarIconSize, controller: controller, ), + if (showSmallButton) + ToggleStyleButton( + attribute: Attribute.small, + icon: Icons.format_size, + iconSize: toolbarIconSize, + controller: controller, + ), if (showUnderLineButton) ToggleStyleButton( attribute: Attribute.underline, From 3378ddff5773f5a36b597574fa8fed6b25b169eb Mon Sep 17 00:00:00 2001 From: Xun Gong Date: Wed, 1 Sep 2021 18:54:22 -0700 Subject: [PATCH 007/179] Revert caret position change which breaks its position --- lib/src/widgets/cursor.dart | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/lib/src/widgets/cursor.dart b/lib/src/widgets/cursor.dart index 2d13aff0..427158d2 100644 --- a/lib/src/widgets/cursor.dart +++ b/lib/src/widgets/cursor.dart @@ -246,18 +246,8 @@ class CursorPainter { /// [offset] is global top left (x, y) of text line /// [position] is relative (x) in text line void paint(Canvas canvas, Offset offset, TextPosition position) { - // relative (x, y) to global offset - var relativeCaretOffset = editable!.getOffsetForCaret(position, prototype); - if (relativeCaretOffset == Offset.zero) { - relativeCaretOffset = editable!.getOffsetForCaret( - TextPosition( - offset: position.offset - 1, affinity: position.affinity), - prototype); - // Hardcoded 6 as estimate of the width of a character - relativeCaretOffset = - Offset(relativeCaretOffset.dx + 6, relativeCaretOffset.dy); - } - final caretOffset = relativeCaretOffset + offset; + final caretOffset = + editable!.getOffsetForCaret(position, prototype) + offset;; var caretRect = prototype.shift(caretOffset); if (style.offset != null) { caretRect = caretRect.shift(style.offset!); From 8945379379daa95c6714f69e5fbcaf2243e359b5 Mon Sep 17 00:00:00 2001 From: Xun Gong Date: Wed, 1 Sep 2021 18:59:19 -0700 Subject: [PATCH 008/179] Default small text to dark gray color --- lib/src/widgets/default_styles.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/widgets/default_styles.dart b/lib/src/widgets/default_styles.dart index ca00cf19..8f36fac1 100644 --- a/lib/src/widgets/default_styles.dart +++ b/lib/src/widgets/default_styles.dart @@ -149,7 +149,7 @@ class DefaultStyles { baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null), bold: const TextStyle(fontWeight: FontWeight.bold), italic: const TextStyle(fontStyle: FontStyle.italic), - small: const TextStyle(fontSize: 12), + small: const TextStyle(fontSize: 12, color: Colors.black45), underline: const TextStyle(decoration: TextDecoration.underline), strikeThrough: const TextStyle(decoration: TextDecoration.lineThrough), link: TextStyle( From 97ba9cfb39dd444119b7e615852ba2e94fc1dbc9 Mon Sep 17 00:00:00 2001 From: Aldy J Date: Tue, 7 Sep 2021 10:03:48 +0700 Subject: [PATCH 009/179] Text Alignment functions + Block Format standards (#382) * Mute problem reports * For PR: Text Alignment functions + Block Format standards * Restore analysis_options.yaml to original settings --- example/lib/pages/home_page.dart | 17 ++- .../macos/Runner/DebugProfile.entitlements | 2 + .../flutter/generated_plugin_registrant.cc | 6 +- lib/src/models/documents/attribute.dart | 7 + lib/src/models/documents/nodes/line.dart | 17 ++- lib/src/models/rules/format.dart | 29 +++- lib/src/widgets/raw_editor.dart | 6 +- lib/src/widgets/simple_viewer.dart | 6 +- lib/src/widgets/text_block.dart | 7 +- lib/src/widgets/text_line.dart | 15 +- lib/src/widgets/toolbar.dart | 34 ++++- .../toolbar/select_alignment_button.dart | 129 ++++++++++++++++++ .../toolbar/toggle_check_list_button.dart | 6 +- .../widgets/toolbar/toggle_style_button.dart | 6 +- 14 files changed, 251 insertions(+), 36 deletions(-) create mode 100644 lib/src/widgets/toolbar/select_alignment_button.dart diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 8abf61d9..36acd869 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -149,18 +149,23 @@ class _HomePageState extends State { onVideoPickCallback: _onVideoPickCallback, // uncomment to provide a custom "pick from" dialog. // mediaPickSettingSelector: _selectMediaPickSetting, + showAlignmentButtons: true, ); if (kIsWeb) { toolbar = QuillToolbar.basic( - controller: _controller!, - onImagePickCallback: _onImagePickCallback, - webImagePickImpl: _webImagePickImpl); + controller: _controller!, + onImagePickCallback: _onImagePickCallback, + webImagePickImpl: _webImagePickImpl, + showAlignmentButtons: true, + ); } if (_isDesktop()) { toolbar = QuillToolbar.basic( - controller: _controller!, - onImagePickCallback: _onImagePickCallback, - filePickImpl: openFileSystemPickerForDesktop); + controller: _controller!, + onImagePickCallback: _onImagePickCallback, + filePickImpl: openFileSystemPickerForDesktop, + showAlignmentButtons: true, + ); } return SafeArea( diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements index 51d09670..d6c8ee0c 100644 --- a/example/macos/Runner/DebugProfile.entitlements +++ b/example/macos/Runner/DebugProfile.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.server + com.apple.security.network.client + diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc index ddfcf7c3..92772e9f 100644 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -4,9 +4,9 @@ #include "generated_plugin_registrant.h" -#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { - UrlLauncherPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/lib/src/models/documents/attribute.dart b/lib/src/models/documents/attribute.dart index 467742d5..f52f70b7 100644 --- a/lib/src/models/documents/attribute.dart +++ b/lib/src/models/documents/attribute.dart @@ -111,6 +111,13 @@ class Attribute { Attribute.indent.key, }); + static final Set exclusiveBlockKeys = LinkedHashSet.of({ + Attribute.header.key, + Attribute.list.key, + Attribute.codeBlock.key, + Attribute.blockQuote.key, + }); + static Attribute get h1 => HeaderAttribute(level: 1); static Attribute get h2 => HeaderAttribute(level: 2); diff --git a/lib/src/models/documents/nodes/line.dart b/lib/src/models/documents/nodes/line.dart index fa5f6c90..bccd7fb8 100644 --- a/lib/src/models/documents/nodes/line.dart +++ b/lib/src/models/documents/nodes/line.dart @@ -202,11 +202,26 @@ class Line extends Container { if (parent is Block) { final parentStyle = (parent as Block).style.getBlocksExceptHeader(); - if (blockStyle.value == null) { + // Ensure that we're only unwrapping the block only if we unset a single + // block format in the `parentStyle` and there are no more block formats + // left to unset. + if (blockStyle.value == null && + parentStyle.containsKey(blockStyle.key) && + parentStyle.length == 1) { _unwrap(); } else if (!const MapEquality() .equals(newStyle.getBlocksExceptHeader(), parentStyle)) { _unwrap(); + // Block style now can contain multiple attributes + if (newStyle.attributes.keys + .any(Attribute.exclusiveBlockKeys.contains)) { + parentStyle.removeWhere( + (key, attr) => Attribute.exclusiveBlockKeys.contains(key)); + } + parentStyle.removeWhere( + (key, attr) => newStyle?.attributes.keys.contains(key) ?? false); + final parentStyleToMerge = Style.attr(parentStyle); + newStyle = newStyle.mergeAll(parentStyleToMerge); _applyBlockStyles(newStyle); } // else the same style, no-op. } else if (blockStyle.value != null) { diff --git a/lib/src/models/rules/format.dart b/lib/src/models/rules/format.dart index be201925..38124af8 100644 --- a/lib/src/models/rules/format.dart +++ b/lib/src/models/rules/format.dart @@ -39,10 +39,23 @@ class ResolveLineFormatRule extends FormatRule { final tmp = Delta(); var offset = 0; + // Enforce Block Format exclusivity by rule + final removedBlocks = Attribute.exclusiveBlockKeys.contains(attribute.key) + ? op.attributes?.keys + .where((key) => + Attribute.exclusiveBlockKeys.contains(key) && + attribute.key != key && + attribute.value != null) + .map((key) => MapEntry(key, null)) ?? + [] + : >[]; + for (var lineBreak = text.indexOf('\n'); lineBreak >= 0; lineBreak = text.indexOf('\n', offset)) { - tmp..retain(lineBreak - offset)..retain(1, attribute.toJson()); + tmp + ..retain(lineBreak - offset) + ..retain(1, attribute.toJson()..addEntries(removedBlocks)); offset = lineBreak + 1; } tmp.retain(text.length - offset); @@ -57,7 +70,19 @@ class ResolveLineFormatRule extends FormatRule { delta.retain(op.length!); continue; } - delta..retain(lineBreak)..retain(1, attribute.toJson()); + // Enforce Block Format exclusivity by rule + final removedBlocks = Attribute.exclusiveBlockKeys.contains(attribute.key) + ? op.attributes?.keys + .where((key) => + Attribute.exclusiveBlockKeys.contains(key) && + attribute.key != key && + attribute.value != null) + .map((key) => MapEntry(key, null)) ?? + [] + : >[]; + delta + ..retain(lineBreak) + ..retain(1, attribute.toJson()..addEntries(removedBlocks)); break; } return delta; diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 09b4940b..d88a49e5 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -313,8 +313,12 @@ class RawEditorState extends EditorState return defaultStyles!.code!.verticalSpacing; } else if (attrs.containsKey(Attribute.indent.key)) { return defaultStyles!.indent!.verticalSpacing; + } else if (attrs.containsKey(Attribute.list.key)) { + return defaultStyles!.lists!.verticalSpacing; + } else if (attrs.containsKey(Attribute.align.key)) { + return defaultStyles!.align!.verticalSpacing; } - return defaultStyles!.lists!.verticalSpacing; + return const Tuple2(0, 0); } @override diff --git a/lib/src/widgets/simple_viewer.dart b/lib/src/widgets/simple_viewer.dart index 2d5fa451..8d59686a 100644 --- a/lib/src/widgets/simple_viewer.dart +++ b/lib/src/widgets/simple_viewer.dart @@ -295,8 +295,12 @@ class _QuillSimpleViewerState extends State return defaultStyles!.code!.verticalSpacing; } else if (attrs.containsKey(Attribute.indent.key)) { return defaultStyles!.indent!.verticalSpacing; + } else if (attrs.containsKey(Attribute.list.key)) { + return defaultStyles!.lists!.verticalSpacing; + } else if (attrs.containsKey(Attribute.align.key)) { + return defaultStyles!.align!.verticalSpacing; } - return defaultStyles!.lists!.verticalSpacing; + return const Tuple2(0, 0); } void _nullSelectionChanged( diff --git a/lib/src/widgets/text_block.dart b/lib/src/widgets/text_block.dart index e668d30b..64d00cfb 100644 --- a/lib/src/widgets/text_block.dart +++ b/lib/src/widgets/text_block.dart @@ -213,11 +213,16 @@ class EditableTextBlock extends StatelessWidget { extraIndent = 16.0 * indent.value; } + var baseIndent = 0.0; + if (attrs.containsKey(Attribute.blockQuote.key)) { return 16.0 + extraIndent; + } else if (attrs.containsKey(Attribute.list.key) || + attrs.containsKey(Attribute.codeBlock.key)) { + baseIndent = 32.0; } - return 32.0 + extraIndent; + return baseIndent + extraIndent; } Tuple2 _getSpacingForLine( diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index 6da21a4c..fb700e26 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -104,11 +104,11 @@ class TextLine extends StatelessWidget { TextAlign _getTextAlign() { final alignment = line.style.attributes[Attribute.align.key]; if (alignment == Attribute.leftAlignment) { - return TextAlign.left; + return TextAlign.start; } else if (alignment == Attribute.centerAlignment) { return TextAlign.center; } else if (alignment == Attribute.rightAlignment) { - return TextAlign.right; + return TextAlign.end; } else if (alignment == Attribute.justifyAlignment) { return TextAlign.justify; } @@ -140,13 +140,20 @@ class TextLine extends StatelessWidget { textStyle = textStyle.merge(m[header] ?? defaultStyles.paragraph!.style); - final block = line.style.getBlockExceptHeader(); + // Only retrieve exclusive block format for the line style purpose + Attribute? block; + line.style.getBlocksExceptHeader().forEach((key, value) { + if (Attribute.exclusiveBlockKeys.contains(key)) { + block = value; + } + }); + TextStyle? toMerge; if (block == Attribute.blockQuote) { toMerge = defaultStyles.quote!.style; } else if (block == Attribute.codeBlock) { toMerge = defaultStyles.code!.style; - } else if (block != null) { + } else if (block == Attribute.list) { toMerge = defaultStyles.lists!.style; } diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 2803948d..80e04e01 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -14,6 +14,7 @@ import 'toolbar/image_button.dart'; import 'toolbar/indent_button.dart'; import 'toolbar/insert_embed_button.dart'; import 'toolbar/link_style_button.dart'; +import 'toolbar/select_alignment_button.dart'; import 'toolbar/select_header_style_button.dart'; import 'toolbar/toggle_check_list_button.dart'; import 'toolbar/toggle_style_button.dart'; @@ -29,6 +30,7 @@ export 'toolbar/insert_embed_button.dart'; export 'toolbar/link_style_button.dart'; export 'toolbar/quill_dropdown_button.dart'; export 'toolbar/quill_icon_button.dart'; +export 'toolbar/select_alignment_button.dart'; export 'toolbar/select_header_style_button.dart'; export 'toolbar/toggle_check_list_button.dart'; export 'toolbar/toggle_style_button.dart'; @@ -71,6 +73,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { bool showColorButton = true, bool showBackgroundColorButton = true, bool showClearFormat = true, + bool showAlignmentButtons = false, bool showHeaderStyle = true, bool showListNumbers = true, bool showListBullets = true, @@ -105,6 +108,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { showClearFormat || onImagePickCallback != null || onVideoPickCallback != null, + showAlignmentButtons, showHeaderStyle, showListNumbers || showListBullets || showListCheck || showCodeBlock, showQuote || showIndent, @@ -220,21 +224,37 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { (isButtonGroupShown[1] || isButtonGroupShown[2] || isButtonGroupShown[3] || - isButtonGroupShown[4])) + isButtonGroupShown[4] || + isButtonGroupShown[5])) VerticalDivider( indent: 12, endIndent: 12, color: Colors.grey.shade400, ), - if (showHeaderStyle) - SelectHeaderStyleButton( + if (showAlignmentButtons) + SelectAlignmentButton( controller: controller, iconSize: toolbarIconSize, ), if (isButtonGroupShown[1] && (isButtonGroupShown[2] || isButtonGroupShown[3] || - isButtonGroupShown[4])) + isButtonGroupShown[4] || + isButtonGroupShown[5])) + VerticalDivider( + indent: 12, + endIndent: 12, + color: Colors.grey.shade400, + ), + if (showHeaderStyle) + SelectHeaderStyleButton( + controller: controller, + iconSize: toolbarIconSize, + ), + if (isButtonGroupShown[2] && + (isButtonGroupShown[3] || + isButtonGroupShown[4] || + isButtonGroupShown[5])) VerticalDivider( indent: 12, endIndent: 12, @@ -268,8 +288,8 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { icon: Icons.code, iconSize: toolbarIconSize, ), - if (isButtonGroupShown[2] && - (isButtonGroupShown[3] || isButtonGroupShown[4])) + if (isButtonGroupShown[3] && + (isButtonGroupShown[4] || isButtonGroupShown[5])) VerticalDivider( indent: 12, endIndent: 12, @@ -296,7 +316,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { controller: controller, isIncrease: false, ), - if (isButtonGroupShown[3] && isButtonGroupShown[4]) + if (isButtonGroupShown[4] && isButtonGroupShown[5]) VerticalDivider( indent: 12, endIndent: 12, diff --git a/lib/src/widgets/toolbar/select_alignment_button.dart b/lib/src/widgets/toolbar/select_alignment_button.dart new file mode 100644 index 00000000..37e5e72e --- /dev/null +++ b/lib/src/widgets/toolbar/select_alignment_button.dart @@ -0,0 +1,129 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../models/documents/attribute.dart'; +import '../../models/documents/style.dart'; +import '../controller.dart'; +import '../toolbar.dart'; + +class SelectAlignmentButton extends StatefulWidget { + const SelectAlignmentButton({ + required this.controller, + this.iconSize = kDefaultIconSize, + Key? key, + }) : super(key: key); + + final QuillController controller; + final double iconSize; + + @override + _SelectAlignmentButtonState createState() => _SelectAlignmentButtonState(); +} + +class _SelectAlignmentButtonState extends State { + Attribute? _value; + + Style get _selectionStyle => widget.controller.getSelectionStyle(); + + @override + void initState() { + super.initState(); + setState(() { + _value = _selectionStyle.attributes[Attribute.align.key] ?? + Attribute.leftAlignment; + }); + widget.controller.addListener(_didChangeEditingValue); + } + + @override + Widget build(BuildContext context) { + final _valueToText = { + Attribute.leftAlignment: Attribute.leftAlignment.value!, + Attribute.centerAlignment: Attribute.centerAlignment.value!, + Attribute.rightAlignment: Attribute.rightAlignment.value!, + Attribute.justifyAlignment: Attribute.justifyAlignment.value!, + }; + + final _valueAttribute = [ + Attribute.leftAlignment, + Attribute.centerAlignment, + Attribute.rightAlignment, + Attribute.justifyAlignment + ]; + final _valueString = [ + Attribute.leftAlignment.value!, + Attribute.centerAlignment.value!, + Attribute.rightAlignment.value!, + Attribute.justifyAlignment.value!, + ]; + + final theme = Theme.of(context); + + return Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(4, (index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0), + child: ConstrainedBox( + constraints: BoxConstraints.tightFor( + width: widget.iconSize * kIconButtonFactor, + height: widget.iconSize * kIconButtonFactor, + ), + child: RawMaterialButton( + hoverElevation: 0, + highlightElevation: 0, + elevation: 0, + visualDensity: VisualDensity.compact, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2)), + fillColor: _valueToText[_value] == _valueString[index] + ? theme.toggleableActiveColor + : theme.canvasColor, + onPressed: () => _valueAttribute[index] == Attribute.leftAlignment + ? widget.controller + .formatSelection(Attribute.clone(Attribute.align, null)) + : widget.controller.formatSelection(_valueAttribute[index]), + child: Icon( + _valueString[index] == Attribute.leftAlignment.value + ? Icons.format_align_left + : _valueString[index] == Attribute.centerAlignment.value + ? Icons.format_align_center + : _valueString[index] == Attribute.rightAlignment.value + ? Icons.format_align_right + : Icons.format_align_justify, + size: widget.iconSize, + color: _valueToText[_value] == _valueString[index] + ? theme.primaryIconTheme.color + : theme.iconTheme.color, + ), + ), + ), + ); + }), + ); + } + + void _didChangeEditingValue() { + setState(() { + _value = _selectionStyle.attributes[Attribute.align.key] ?? + Attribute.leftAlignment; + }); + } + + @override + void didUpdateWidget(covariant SelectAlignmentButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + oldWidget.controller.removeListener(_didChangeEditingValue); + widget.controller.addListener(_didChangeEditingValue); + _value = _selectionStyle.attributes[Attribute.align.key] ?? + Attribute.leftAlignment; + } + } + + @override + void dispose() { + widget.controller.removeListener(_didChangeEditingValue); + super.dispose(); + } +} diff --git a/lib/src/widgets/toolbar/toggle_check_list_button.dart b/lib/src/widgets/toolbar/toggle_check_list_button.dart index 861da445..c147e108 100644 --- a/lib/src/widgets/toolbar/toggle_check_list_button.dart +++ b/lib/src/widgets/toolbar/toggle_check_list_button.dart @@ -81,17 +81,13 @@ class _ToggleCheckListButtonState extends State { @override Widget build(BuildContext context) { - final isInCodeBlock = - _selectionStyle.attributes.containsKey(Attribute.codeBlock.key); - final isEnabled = - !isInCodeBlock || Attribute.list.key == Attribute.codeBlock.key; return widget.childBuilder( context, Attribute.unchecked, widget.icon, widget.fillColor, _isToggled, - isEnabled ? _toggleAttribute : null, + _toggleAttribute, widget.iconSize, ); } diff --git a/lib/src/widgets/toolbar/toggle_style_button.dart b/lib/src/widgets/toolbar/toggle_style_button.dart index 624a31f9..8299f8a4 100644 --- a/lib/src/widgets/toolbar/toggle_style_button.dart +++ b/lib/src/widgets/toolbar/toggle_style_button.dart @@ -56,17 +56,13 @@ class _ToggleStyleButtonState extends State { @override Widget build(BuildContext context) { - final isInCodeBlock = - _selectionStyle.attributes.containsKey(Attribute.codeBlock.key); - final isEnabled = - !isInCodeBlock || widget.attribute.key == Attribute.codeBlock.key; return widget.childBuilder( context, widget.attribute, widget.icon, widget.fillColor, _isToggled, - isEnabled ? _toggleAttribute : null, + _toggleAttribute, widget.iconSize, ); } From 40e68ac3ec24d7cae1eb6c033c17e2779bdae984 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Mon, 6 Sep 2021 20:07:21 -0700 Subject: [PATCH 010/179] Upgrade to 2.0.0 --- CHANGELOG.md | 3 +++ lib/src/widgets/cursor.dart | 3 ++- lib/src/widgets/text_block.dart | 8 +++++--- pubspec.yaml | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 427d4c95..7fc19bab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.0] +* Text Alignment functions + Block Format standards. + ## [1.9.6] * Support putting QuillEditor inside a Scrollable view. diff --git a/lib/src/widgets/cursor.dart b/lib/src/widgets/cursor.dart index 427158d2..0365d317 100644 --- a/lib/src/widgets/cursor.dart +++ b/lib/src/widgets/cursor.dart @@ -247,7 +247,8 @@ class CursorPainter { /// [position] is relative (x) in text line void paint(Canvas canvas, Offset offset, TextPosition position) { final caretOffset = - editable!.getOffsetForCaret(position, prototype) + offset;; + editable!.getOffsetForCaret(position, prototype) + offset; + var caretRect = prototype.shift(caretOffset); if (style.offset != null) { caretRect = caretRect.shift(style.offset!); diff --git a/lib/src/widgets/text_block.dart b/lib/src/widgets/text_block.dart index 64d00cfb..e46cba0d 100644 --- a/lib/src/widgets/text_block.dart +++ b/lib/src/widgets/text_block.dart @@ -213,11 +213,13 @@ class EditableTextBlock extends StatelessWidget { extraIndent = 16.0 * indent.value; } - var baseIndent = 0.0; - if (attrs.containsKey(Attribute.blockQuote.key)) { return 16.0 + extraIndent; - } else if (attrs.containsKey(Attribute.list.key) || + } + + var baseIndent = 0.0; + + if (attrs.containsKey(Attribute.list.key) || attrs.containsKey(Attribute.codeBlock.key)) { baseIndent = 32.0; } diff --git a/pubspec.yaml b/pubspec.yaml index 4a442472..fc49b2e1 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.9.6 +version: 2.0.0 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 6566fb71773a7eaeb06d957486e531ebb46099d5 Mon Sep 17 00:00:00 2001 From: Raihan Iyai Date: Thu, 9 Sep 2021 03:44:45 +0700 Subject: [PATCH 011/179] update colorpicker to prevent error xcode build (#315) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index fc49b2e1..f3a9abb2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: flutter: sdk: flutter collection: ^1.15.0 - flutter_colorpicker: ^0.4.0 + flutter_colorpicker: ^0.5.0 flutter_keyboard_visibility: ^5.0.0 image_picker: ^0.8.2 photo_view: ^0.12.0 From 15029c252b92ccadf5178702f468d8f44caf60c9 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 8 Sep 2021 13:46:28 -0700 Subject: [PATCH 012/179] Upgrade to [2.0.1] Upgrade flutter_colorpicker to 0.5.0 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fc19bab..85f7043a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.1] +* Upgrade flutter_colorpicker to 0.5.0. + ## [2.0.0] * Text Alignment functions + Block Format standards. diff --git a/pubspec.yaml b/pubspec.yaml index f3a9abb2..b7aa6db9 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: 2.0.0 +version: 2.0.1 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From feec35386eb907eabae22f6cabd2dfac22427d1e Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 8 Sep 2021 13:50:59 -0700 Subject: [PATCH 013/179] Adapt to Flutter Channel stable, 2.5.0 --- example/test/widget_test.dart | 2 +- lib/src/widgets/default_styles.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index bf233450..ca3ecf17 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -5,9 +5,9 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. +import 'package:app/main.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:app/main.dart'; void main() { testWidgets('Counter increments smoke test', (tester) async { diff --git a/lib/src/widgets/default_styles.dart b/lib/src/widgets/default_styles.dart index 8f36fac1..4c84966e 100644 --- a/lib/src/widgets/default_styles.dart +++ b/lib/src/widgets/default_styles.dart @@ -153,7 +153,7 @@ class DefaultStyles { underline: const TextStyle(decoration: TextDecoration.underline), strikeThrough: const TextStyle(decoration: TextDecoration.lineThrough), link: TextStyle( - color: themeData.accentColor, + color: themeData.colorScheme.secondary, decoration: TextDecoration.underline, ), placeHolder: DefaultTextBlockStyle( From 52f5193819eed5308516e0eeafb679a448141e44 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 8 Sep 2021 13:58:12 -0700 Subject: [PATCH 014/179] Address KeyboardListener class name conflict --- lib/src/models/rules/delete.dart | 4 +++- lib/src/models/rules/format.dart | 8 ++++++-- lib/src/models/rules/insert.dart | 4 +++- lib/src/widgets/keyboard_listener.dart | 4 ++-- lib/src/widgets/raw_editor.dart | 4 ++-- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/src/models/rules/delete.dart b/lib/src/models/rules/delete.dart index e6682f94..06c057eb 100644 --- a/lib/src/models/rules/delete.dart +++ b/lib/src/models/rules/delete.dart @@ -66,7 +66,9 @@ class PreserveLineStyleOnMergeRule extends DeleteRule { attributes ??= {}; attributes.addAll(attrs!); } - delta..retain(lineBreak)..retain(1, attributes); + delta + ..retain(lineBreak) + ..retain(1, attributes); break; } return delta; diff --git a/lib/src/models/rules/format.dart b/lib/src/models/rules/format.dart index 38124af8..62a41317 100644 --- a/lib/src/models/rules/format.dart +++ b/lib/src/models/rules/format.dart @@ -114,7 +114,9 @@ class FormatLinkAtCaretPositionRule extends FormatRule { return null; } - delta..retain(beg)..retain(retain!, attribute.toJson()); + delta + ..retain(beg) + ..retain(retain!, attribute.toJson()); return delta; } } @@ -143,7 +145,9 @@ class ResolveInlineFormatRule extends FormatRule { } var pos = 0; while (lineBreak >= 0) { - delta..retain(lineBreak - pos, attribute.toJson())..retain(1); + delta + ..retain(lineBreak - pos, attribute.toJson()) + ..retain(1); pos = lineBreak + 1; lineBreak = text.indexOf('\n', pos); } diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart index 892b3e8a..67522d38 100644 --- a/lib/src/models/rules/insert.dart +++ b/lib/src/models/rules/insert.dart @@ -192,7 +192,9 @@ class AutoExitBlockRule extends InsertRule { attributes.keys.firstWhere(Attribute.blockKeysExceptHeader.contains); attributes[k] = null; // retain(1) should be '\n', set it with no attribute - return Delta()..retain(index + (len ?? 0))..retain(1, attributes); + return Delta() + ..retain(index + (len ?? 0)) + ..retain(1, attributes); } } diff --git a/lib/src/widgets/keyboard_listener.dart b/lib/src/widgets/keyboard_listener.dart index ceabf931..782321db 100644 --- a/lib/src/widgets/keyboard_listener.dart +++ b/lib/src/widgets/keyboard_listener.dart @@ -9,8 +9,8 @@ typedef CursorMoveCallback = void Function( typedef InputShortcutCallback = void Function(InputShortcut? shortcut); typedef OnDeleteCallback = void Function(bool forward); -class KeyboardListener { - KeyboardListener(this.onCursorMove, this.onShortcut, this.onDelete); +class CustomKeyboardListener { + CustomKeyboardListener(this.onCursorMove, this.onShortcut, this.onDelete); final CursorMoveCallback onCursorMove; final InputShortcutCallback onShortcut; diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index d88a49e5..a67f6f3a 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -106,7 +106,7 @@ class RawEditorState extends EditorState final GlobalKey _editorKey = GlobalKey(); // Keyboard - late KeyboardListener _keyboardListener; + late CustomKeyboardListener _keyboardListener; KeyboardVisibilityController? _keyboardVisibilityController; StreamSubscription? _keyboardVisibilitySubscription; bool _keyboardVisible = false; @@ -340,7 +340,7 @@ class RawEditorState extends EditorState tickerProvider: this, ); - _keyboardListener = KeyboardListener( + _keyboardListener = CustomKeyboardListener( handleCursorMovement, handleShortcut, handleDelete, From a13ded263b8568153e65c89dea57b6d1ea29deb7 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 8 Sep 2021 14:00:43 -0700 Subject: [PATCH 015/179] Upgrade to 2.0.2 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85f7043a..382d4f01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.2] +* Address KeyboardListener class name conflict. + ## [2.0.1] * Upgrade flutter_colorpicker to 0.5.0. diff --git a/pubspec.yaml b/pubspec.yaml index b7aa6db9..aa9753b6 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: 2.0.1 +version: 2.0.2 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 5b230a718b9d5a81398a25e48cbde3ef8b2bb331 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 8 Sep 2021 21:58:39 -0700 Subject: [PATCH 016/179] Rename CustomeKeyboardListener to KeyboardEventHandler --- lib/src/widgets/keyboard_listener.dart | 14 ++++++++------ lib/src/widgets/raw_editor.dart | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/src/widgets/keyboard_listener.dart b/lib/src/widgets/keyboard_listener.dart index 782321db..b1b868aa 100644 --- a/lib/src/widgets/keyboard_listener.dart +++ b/lib/src/widgets/keyboard_listener.dart @@ -9,8 +9,8 @@ typedef CursorMoveCallback = void Function( typedef InputShortcutCallback = void Function(InputShortcut? shortcut); typedef OnDeleteCallback = void Function(bool forward); -class CustomKeyboardListener { - CustomKeyboardListener(this.onCursorMove, this.onShortcut, this.onDelete); +class KeyboardEventHandler { + KeyboardEventHandler(this.onCursorMove, this.onShortcut, this.onDelete); final CursorMoveCallback onCursorMove; final InputShortcutCallback onShortcut; @@ -86,21 +86,23 @@ class CustomKeyboardListener { return KeyEventResult.ignored; } + final isShortcutModifierPressed = + isMacOS ? event.isMetaPressed : event.isControlPressed; if (_moveKeys.contains(key)) { onCursorMove( key, isMacOS ? event.isAltPressed : event.isControlPressed, isMacOS ? event.isMetaPressed : event.isAltPressed, event.isShiftPressed); - } else if (isMacOS - ? event.isMetaPressed - : event.isControlPressed && _shortcutKeys.contains(key)) { + } else if (isShortcutModifierPressed && _shortcutKeys.contains(key)) { onShortcut(_keyToShortcut[key]); } else if (key == LogicalKeyboardKey.delete) { onDelete(true); } else if (key == LogicalKeyboardKey.backspace) { onDelete(false); + } else { + return KeyEventResult.ignored; } - return KeyEventResult.ignored; + return KeyEventResult.handled; } } diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index a67f6f3a..66330d55 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -106,7 +106,7 @@ class RawEditorState extends EditorState final GlobalKey _editorKey = GlobalKey(); // Keyboard - late CustomKeyboardListener _keyboardListener; + late KeyboardEventHandler _keyboardListener; KeyboardVisibilityController? _keyboardVisibilityController; StreamSubscription? _keyboardVisibilitySubscription; bool _keyboardVisible = false; @@ -340,7 +340,7 @@ class RawEditorState extends EditorState tickerProvider: this, ); - _keyboardListener = CustomKeyboardListener( + _keyboardListener = KeyboardEventHandler( handleCursorMovement, handleShortcut, handleDelete, From 2b572ca4095e166f374d986b4cc9b3ac4e7848b5 Mon Sep 17 00:00:00 2001 From: li3317 Date: Fri, 10 Sep 2021 10:24:09 -0400 Subject: [PATCH 017/179] fix cursor when line contains image --- lib/src/widgets/cursor.dart | 18 ++++++++++++++++-- lib/src/widgets/text_line.dart | 8 ++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/src/widgets/cursor.dart b/lib/src/widgets/cursor.dart index 0365d317..d315f785 100644 --- a/lib/src/widgets/cursor.dart +++ b/lib/src/widgets/cursor.dart @@ -245,9 +245,23 @@ class CursorPainter { /// Paints cursor on [canvas] at specified [position]. /// [offset] is global top left (x, y) of text line /// [position] is relative (x) in text line - void paint(Canvas canvas, Offset offset, TextPosition position) { - final caretOffset = + void paint(Canvas canvas, Offset offset, TextPosition position, bool lineHasEmbed) { + var caretOffset = editable!.getOffsetForCaret(position, prototype) + offset; + if (lineHasEmbed) { + // relative (x, y) to global offset + var relativeCaretOffset = editable!.getOffsetForCaret(position, prototype); + if (relativeCaretOffset == Offset.zero) { + relativeCaretOffset = editable!.getOffsetForCaret( + TextPosition( + offset: position.offset - 1, affinity: position.affinity), + prototype); + // Hardcoded 6 as estimate of the width of a character + relativeCaretOffset = + Offset(relativeCaretOffset.dx + 6, relativeCaretOffset.dy); + } + caretOffset = relativeCaretOffset + offset; + } var caretRect = prototype.shift(caretOffset); if (style.offset != null) { diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index fb700e26..e4f69a5e 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -825,7 +825,7 @@ class RenderEditableTextLine extends RenderEditableBox { cursorCont.show.value && containsCursor() && !cursorCont.style.paintAboveText) { - _paintCursor(context, effectiveOffset); + _paintCursor(context, effectiveOffset, line.hasEmbed); } context.paintChild(_body!, effectiveOffset); @@ -834,7 +834,7 @@ class RenderEditableTextLine extends RenderEditableBox { cursorCont.show.value && containsCursor() && cursorCont.style.paintAboveText) { - _paintCursor(context, effectiveOffset); + _paintCursor(context, effectiveOffset, line.hasEmbed); } } } @@ -847,12 +847,12 @@ class RenderEditableTextLine extends RenderEditableBox { } } - void _paintCursor(PaintingContext context, Offset effectiveOffset) { + void _paintCursor(PaintingContext context, Offset effectiveOffset, bool lineHasEmbed) { final position = TextPosition( offset: textSelection.extentOffset - line.documentOffset, affinity: textSelection.base.affinity, ); - _cursorPainter.paint(context.canvas, effectiveOffset, position); + _cursorPainter.paint(context.canvas, effectiveOffset, position, lineHasEmbed); } @override From 2009a0d1ef306925f908d1a45ef5fe5f58458b16 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 10 Sep 2021 09:35:30 -0700 Subject: [PATCH 018/179] Upgrade to 2.0.3 --- CHANGELOG.md | 3 +++ lib/src/widgets/cursor.dart | 29 +++++++++++++---------------- lib/src/widgets/text_line.dart | 6 ++++-- pubspec.yaml | 2 +- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 382d4f01..1d451d3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.3] +* Fix cursor when line contains image. + ## [2.0.2] * Address KeyboardListener class name conflict. diff --git a/lib/src/widgets/cursor.dart b/lib/src/widgets/cursor.dart index d315f785..dd449c7c 100644 --- a/lib/src/widgets/cursor.dart +++ b/lib/src/widgets/cursor.dart @@ -245,24 +245,21 @@ class CursorPainter { /// Paints cursor on [canvas] at specified [position]. /// [offset] is global top left (x, y) of text line /// [position] is relative (x) in text line - void paint(Canvas canvas, Offset offset, TextPosition position, bool lineHasEmbed) { - var caretOffset = - editable!.getOffsetForCaret(position, prototype) + offset; - if (lineHasEmbed) { - // relative (x, y) to global offset - var relativeCaretOffset = editable!.getOffsetForCaret(position, prototype); - if (relativeCaretOffset == Offset.zero) { - relativeCaretOffset = editable!.getOffsetForCaret( - TextPosition( - offset: position.offset - 1, affinity: position.affinity), - prototype); - // Hardcoded 6 as estimate of the width of a character - relativeCaretOffset = - Offset(relativeCaretOffset.dx + 6, relativeCaretOffset.dy); - } - caretOffset = relativeCaretOffset + offset; + void paint( + Canvas canvas, Offset offset, TextPosition position, bool lineHasEmbed) { + // relative (x, y) to global offset + var relativeCaretOffset = editable!.getOffsetForCaret(position, prototype); + if (lineHasEmbed && relativeCaretOffset == Offset.zero) { + relativeCaretOffset = editable!.getOffsetForCaret( + TextPosition( + offset: position.offset - 1, affinity: position.affinity), + prototype); + // Hardcoded 6 as estimate of the width of a character + relativeCaretOffset = + Offset(relativeCaretOffset.dx + 6, relativeCaretOffset.dy); } + final caretOffset = relativeCaretOffset + offset; var caretRect = prototype.shift(caretOffset); if (style.offset != null) { caretRect = caretRect.shift(style.offset!); diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index e4f69a5e..f8a3f396 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -847,12 +847,14 @@ class RenderEditableTextLine extends RenderEditableBox { } } - void _paintCursor(PaintingContext context, Offset effectiveOffset, bool lineHasEmbed) { + void _paintCursor( + PaintingContext context, Offset effectiveOffset, bool lineHasEmbed) { final position = TextPosition( offset: textSelection.extentOffset - line.documentOffset, affinity: textSelection.base.affinity, ); - _cursorPainter.paint(context.canvas, effectiveOffset, position, lineHasEmbed); + _cursorPainter.paint( + context.canvas, effectiveOffset, position, lineHasEmbed); } @override diff --git a/pubspec.yaml b/pubspec.yaml index aa9753b6..5139c684 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: 2.0.2 +version: 2.0.3 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 0a67139f18821e15b97145ffd89ecb5a53aac860 Mon Sep 17 00:00:00 2001 From: Dmitry Sboychakov Date: Tue, 14 Sep 2021 09:56:18 +0530 Subject: [PATCH 019/179] Feature: history shortcuts (#400) --- lib/src/widgets/controller.dart | 2 +- lib/src/widgets/keyboard_listener.dart | 27 ++++++++++++++++--- .../raw_editor_state_keyboard_mixin.dart | 12 +++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index 7d2e0714..b3173ed2 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -85,7 +85,7 @@ class QuillController extends ChangeNotifier { } void _handleHistoryChange(int? len) { - if (len! > 0) { + if (len != 0) { // if (this.selection.extentOffset >= document.length) { // // cursor exceeds the length of document, position it in the end // updateSelection( diff --git a/lib/src/widgets/keyboard_listener.dart b/lib/src/widgets/keyboard_listener.dart index b1b868aa..4e755fdf 100644 --- a/lib/src/widgets/keyboard_listener.dart +++ b/lib/src/widgets/keyboard_listener.dart @@ -2,7 +2,19 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -enum InputShortcut { CUT, COPY, PASTE, SELECT_ALL } +//fixme workaround flutter MacOS issue https://github.com/flutter/flutter/issues/75595 +extension _LogicalKeyboardKeyCaseExt on LogicalKeyboardKey { + static const _kUpperToLowerDist = 0x20; + static final _kLowerCaseA = LogicalKeyboardKey.keyA.keyId; + static final _kLowerCaseZ = LogicalKeyboardKey.keyZ.keyId; + + LogicalKeyboardKey toUpperCase() { + if (keyId < _kLowerCaseA || keyId > _kLowerCaseZ) return this; + return LogicalKeyboardKey(keyId - _kUpperToLowerDist); + } +} + +enum InputShortcut { CUT, COPY, PASTE, SELECT_ALL, UNDO, REDO } typedef CursorMoveCallback = void Function( LogicalKeyboardKey key, bool wordModifier, bool lineModifier, bool shift); @@ -28,6 +40,8 @@ class KeyboardEventHandler { LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyV, LogicalKeyboardKey.keyX, + LogicalKeyboardKey.keyZ.toUpperCase(), + LogicalKeyboardKey.keyZ, LogicalKeyboardKey.delete, LogicalKeyboardKey.backspace, }; @@ -88,14 +102,21 @@ class KeyboardEventHandler { final isShortcutModifierPressed = isMacOS ? event.isMetaPressed : event.isControlPressed; + if (_moveKeys.contains(key)) { onCursorMove( key, isMacOS ? event.isAltPressed : event.isControlPressed, isMacOS ? event.isMetaPressed : event.isAltPressed, event.isShiftPressed); - } else if (isShortcutModifierPressed && _shortcutKeys.contains(key)) { - onShortcut(_keyToShortcut[key]); + } else if (isShortcutModifierPressed && (_shortcutKeys.contains(key))) { + if (key == LogicalKeyboardKey.keyZ || + key == LogicalKeyboardKey.keyZ.toUpperCase()) { + onShortcut( + event.isShiftPressed ? InputShortcut.REDO : InputShortcut.UNDO); + } else { + onShortcut(_keyToShortcut[key]); + } } else if (key == LogicalKeyboardKey.delete) { onDelete(true); } else if (key == LogicalKeyboardKey.backspace) { diff --git a/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart index 9ba6502d..db860248 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart @@ -76,6 +76,18 @@ mixin RawEditorStateKeyboardMixin on EditorState { } return; } + if (shortcut == InputShortcut.UNDO) { + if (widget.controller.hasUndo) { + widget.controller.undo(); + } + return; + } + if (shortcut == InputShortcut.REDO) { + if (widget.controller.hasRedo) { + widget.controller.redo(); + } + return; + } if (shortcut == InputShortcut.CUT && !widget.readOnly) { if (!selection.isCollapsed) { final data = selection.textInside(plainText); From f253f582d551c85e46344370b613e8e585bf9066 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Mon, 13 Sep 2021 21:34:51 -0700 Subject: [PATCH 020/179] Upgrade to 2.0.4 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d451d3a..188c2304 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.4] +* Enable history shortcuts for desktop. + ## [2.0.3] * Fix cursor when line contains image. diff --git a/pubspec.yaml b/pubspec.yaml index 5139c684..eda8afff 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: 2.0.3 +version: 2.0.4 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 2f298c6672e013811c28dd794dcfad0f2af41e47 Mon Sep 17 00:00:00 2001 From: Dmitry Sboychakov Date: Tue, 14 Sep 2021 10:06:55 +0530 Subject: [PATCH 021/179] fix null safety (#401) --- lib/src/widgets/controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index b3173ed2..15104add 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -85,7 +85,7 @@ class QuillController extends ChangeNotifier { } void _handleHistoryChange(int? len) { - if (len != 0) { + if (len! != 0) { // if (this.selection.extentOffset >= document.length) { // // cursor exceeds the length of document, position it in the end // updateSelection( From 1143f7eb439048a7be5b2c8126a21c02a12e8ab2 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Tue, 14 Sep 2021 20:49:11 -0700 Subject: [PATCH 022/179] Swapping handles order is prevented --- lib/src/widgets/text_selection.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/widgets/text_selection.dart b/lib/src/widgets/text_selection.dart index f11a42c4..833a21e9 100644 --- a/lib/src/widgets/text_selection.dart +++ b/lib/src/widgets/text_selection.dart @@ -407,6 +407,10 @@ class _TextSelectionHandleOverlayState break; } + if (newSelection.baseOffset >= newSelection.extentOffset) { + return; // don't allow order swapping. + } + widget.onSelectionHandleChanged(newSelection); } From 6f32b0eda85a102f9e5e579bca72d33b89761326 Mon Sep 17 00:00:00 2001 From: li3317 Date: Thu, 16 Sep 2021 18:57:46 -0400 Subject: [PATCH 023/179] fix deprecated --- lib/src/widgets/text_selection.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/widgets/text_selection.dart b/lib/src/widgets/text_selection.dart index 833a21e9..16374789 100644 --- a/lib/src/widgets/text_selection.dart +++ b/lib/src/widgets/text_selection.dart @@ -732,7 +732,8 @@ class _EditorTextSelectionGestureDetectorState gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => LongPressGestureRecognizer( - debugOwner: this, kind: PointerDeviceKind.touch), + debugOwner: this, + supportedDevices: {PointerDeviceKind.touch}), (instance) { instance ..onLongPressStart = _handleLongPressStart @@ -748,7 +749,8 @@ class _EditorTextSelectionGestureDetectorState gestures[HorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => HorizontalDragGestureRecognizer( - debugOwner: this, kind: PointerDeviceKind.mouse), + debugOwner: this, + supportedDevices: {PointerDeviceKind.mouse}), (instance) { instance ..dragStartBehavior = DragStartBehavior.down From 19600f7d8fbea0b4a983129874f119847e11f2d4 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sat, 18 Sep 2021 01:23:57 -0700 Subject: [PATCH 024/179] Add inline code formatting --- lib/src/models/documents/attribute.dart | 7 +++++++ lib/src/widgets/default_styles.dart | 8 ++++++++ lib/src/widgets/text_line.dart | 1 + lib/src/widgets/toolbar.dart | 9 +++++++++ 4 files changed, 25 insertions(+) diff --git a/lib/src/models/documents/attribute.dart b/lib/src/models/documents/attribute.dart index f52f70b7..db667ce1 100644 --- a/lib/src/models/documents/attribute.dart +++ b/lib/src/models/documents/attribute.dart @@ -22,6 +22,7 @@ class Attribute { Attribute.small.key: Attribute.small, Attribute.underline.key: Attribute.underline, Attribute.strikeThrough.key: Attribute.strikeThrough, + Attribute.inlineCode.key: Attribute.inlineCode, Attribute.font.key: Attribute.font, Attribute.size.key: Attribute.size, Attribute.link.key: Attribute.link, @@ -50,6 +51,8 @@ class Attribute { static final StrikeThroughAttribute strikeThrough = StrikeThroughAttribute(); + static final InlineCodeAttribute inlineCode = InlineCodeAttribute(); + static final FontAttribute font = FontAttribute(null); static final SizeAttribute size = SizeAttribute(null); @@ -240,6 +243,10 @@ class StrikeThroughAttribute extends Attribute { StrikeThroughAttribute() : super('strike', AttributeScope.INLINE, true); } +class InlineCodeAttribute extends Attribute { + InlineCodeAttribute() : super('code', AttributeScope.INLINE, true); +} + class FontAttribute extends Attribute { FontAttribute(String? val) : super('font', AttributeScope.INLINE, val); } diff --git a/lib/src/widgets/default_styles.dart b/lib/src/widgets/default_styles.dart index 4c84966e..ea583909 100644 --- a/lib/src/widgets/default_styles.dart +++ b/lib/src/widgets/default_styles.dart @@ -54,6 +54,7 @@ class DefaultStyles { this.small, this.underline, this.strikeThrough, + this.inlineCode, this.link, this.color, this.placeHolder, @@ -77,6 +78,7 @@ class DefaultStyles { final TextStyle? small; final TextStyle? underline; final TextStyle? strikeThrough; + final TextStyle? inlineCode; final TextStyle? sizeSmall; // 'small' final TextStyle? sizeLarge; // 'large' final TextStyle? sizeHuge; // 'huge' @@ -152,6 +154,11 @@ class DefaultStyles { small: const TextStyle(fontSize: 12, color: Colors.black45), underline: const TextStyle(decoration: TextDecoration.underline), strikeThrough: const TextStyle(decoration: TextDecoration.lineThrough), + inlineCode: TextStyle( + color: Colors.blue.shade900.withOpacity(0.9), + fontFamily: fontFamily, + fontSize: 13, + ), link: TextStyle( color: themeData.colorScheme.secondary, decoration: TextDecoration.underline, @@ -211,6 +218,7 @@ class DefaultStyles { small: other.small ?? small, underline: other.underline ?? underline, strikeThrough: other.strikeThrough ?? strikeThrough, + inlineCode: other.inlineCode ?? inlineCode, link: other.link ?? link, color: other.color ?? color, placeHolder: other.placeHolder ?? placeHolder, diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index f8a3f396..f87645ab 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -192,6 +192,7 @@ class TextLine extends StatelessWidget { Attribute.link.key: defaultStyles.link, Attribute.underline.key: defaultStyles.underline, Attribute.strikeThrough.key: defaultStyles.strikeThrough, + Attribute.inlineCode.key: defaultStyles.inlineCode, }.forEach((k, s) { if (style.values.any((v) => v.key == k)) { if (k == Attribute.underline.key || k == Attribute.strikeThrough.key) { diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 80e04e01..1fd6b0f1 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -70,6 +70,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { bool showSmallButton = false, bool showUnderLineButton = true, bool showStrikeThrough = true, + bool showInlineCode = true, bool showColorButton = true, bool showBackgroundColorButton = true, bool showClearFormat = true, @@ -103,6 +104,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { showSmallButton || showUnderLineButton || showStrikeThrough || + showInlineCode || showColorButton || showBackgroundColorButton || showClearFormat || @@ -169,6 +171,13 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { iconSize: toolbarIconSize, controller: controller, ), + if (showInlineCode) + ToggleStyleButton( + attribute: Attribute.inlineCode, + icon: Icons.code, + iconSize: toolbarIconSize, + controller: controller, + ), if (showColorButton) ColorButton( icon: Icons.color_lens, From 1956f68806236d30d8aae38b51d26e4acee5b570 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sat, 18 Sep 2021 01:24:57 -0700 Subject: [PATCH 025/179] Upgrade to 2.0.5 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 188c2304..1c31b49f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.5] +* Support inline code formatting. + ## [2.0.4] * Enable history shortcuts for desktop. diff --git a/pubspec.yaml b/pubspec.yaml index eda8afff..ecb52770 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: 2.0.4 +version: 2.0.5 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 6400ba1df5c4a99384251783ff7450cf277f3727 Mon Sep 17 00:00:00 2001 From: Aldy J Date: Sun, 26 Sep 2021 22:19:11 +0700 Subject: [PATCH 026/179] Avoid runtime error when placed inside TabBarView (#410) --- lib/src/widgets/toolbar/arrow_indicated_button_list.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/widgets/toolbar/arrow_indicated_button_list.dart b/lib/src/widgets/toolbar/arrow_indicated_button_list.dart index b17157d4..b3858e5d 100644 --- a/lib/src/widgets/toolbar/arrow_indicated_button_list.dart +++ b/lib/src/widgets/toolbar/arrow_indicated_button_list.dart @@ -60,6 +60,8 @@ class _ArrowIndicatedButtonListState extends State } void _handleScroll() { + if (!mounted) return; + setState(() { _showLeftArrow = _controller.position.minScrollExtent != _controller.position.pixels; From 170752f66d0f4ec376f11024c5215f3c1c3d55d2 Mon Sep 17 00:00:00 2001 From: li3317 Date: Thu, 7 Oct 2021 22:55:11 -0400 Subject: [PATCH 027/179] fix offset --- lib/src/widgets/raw_editor.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 66330d55..899ba5c3 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:math' as math; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -589,7 +590,7 @@ class RawEditorState extends EditorState if (offset != null) { _scrollController.animateTo( - offset, + math.min(offset, _scrollController.position.maxScrollExtent), duration: const Duration(milliseconds: 100), curve: Curves.fastOutSlowIn, ); From 0d83169c07099bc9ce28f0ab9d266d64ff191f4a Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 8 Oct 2021 13:26:56 -0700 Subject: [PATCH 028/179] Upgrade to 2.0.6 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c31b49f..bf25e556 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.6] +* Avoid runtime error when placed inside TabBarView. + ## [2.0.5] * Support inline code formatting. diff --git a/pubspec.yaml b/pubspec.yaml index ecb52770..4216d878 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: 2.0.5 +version: 2.0.6 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 85df4033d876aa127306b04ae2dbb6fcb973b8b5 Mon Sep 17 00:00:00 2001 From: Namli1 <51393783+Namli1@users.noreply.github.com> Date: Sun, 17 Oct 2021 00:32:18 +0200 Subject: [PATCH 029/179] Added theming options for toolbar icons and LinkDialog() (#418) --- lib/flutter_quill.dart | 2 + lib/src/models/themes/quill_dialog_theme.dart | 15 ++++++ lib/src/models/themes/quill_icon_theme.dart | 30 +++++++++++ lib/src/widgets/editor.dart | 19 ++++--- lib/src/widgets/link_dialog.dart | 17 ++++-- lib/src/widgets/toolbar.dart | 52 ++++++++++++++++--- lib/src/widgets/toolbar/camera_button.dart | 12 ++++- .../widgets/toolbar/clear_format_button.dart | 9 +++- lib/src/widgets/toolbar/color_button.dart | 11 ++-- lib/src/widgets/toolbar/history_button.dart | 13 +++-- lib/src/widgets/toolbar/image_button.dart | 18 +++++-- lib/src/widgets/toolbar/indent_button.dart | 12 +++-- .../widgets/toolbar/insert_embed_button.dart | 13 ++++- .../widgets/toolbar/link_style_button.dart | 14 +++-- .../toolbar/select_alignment_button.dart | 16 ++++-- .../toolbar/select_header_style_button.dart | 16 ++++-- .../toolbar/toggle_check_list_button.dart | 5 ++ .../widgets/toolbar/toggle_style_button.dart | 27 +++++++--- lib/src/widgets/toolbar/video_button.dart | 18 +++++-- 19 files changed, 259 insertions(+), 60 deletions(-) create mode 100644 lib/src/models/themes/quill_dialog_theme.dart create mode 100644 lib/src/models/themes/quill_icon_theme.dart diff --git a/lib/flutter_quill.dart b/lib/flutter_quill.dart index 6b6754ce..c82b164f 100644 --- a/lib/flutter_quill.dart +++ b/lib/flutter_quill.dart @@ -5,6 +5,8 @@ export 'src/models/documents/document.dart'; export 'src/models/documents/nodes/embed.dart'; export 'src/models/documents/nodes/leaf.dart'; export 'src/models/quill_delta.dart'; +export 'src/models/themes/quill_dialog_theme.dart'; +export 'src/models/themes/quill_icon_theme.dart'; export 'src/widgets/controller.dart'; export 'src/widgets/default_styles.dart'; export 'src/widgets/editor.dart'; diff --git a/lib/src/models/themes/quill_dialog_theme.dart b/lib/src/models/themes/quill_dialog_theme.dart new file mode 100644 index 00000000..795d35d5 --- /dev/null +++ b/lib/src/models/themes/quill_dialog_theme.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class QuillDialogTheme { + QuillDialogTheme( + {this.labelTextStyle, this.inputTextStyle, this.dialogBackgroundColor}); + + ///The text style to use for the label shown in the link-input dialog + final TextStyle? labelTextStyle; + + ///The text style to use for the input text shown in the link-input dialog + final TextStyle? inputTextStyle; + + ///The background color for the [LinkDialog()] + final Color? dialogBackgroundColor; +} diff --git a/lib/src/models/themes/quill_icon_theme.dart b/lib/src/models/themes/quill_icon_theme.dart new file mode 100644 index 00000000..58f5998b --- /dev/null +++ b/lib/src/models/themes/quill_icon_theme.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class QuillIconTheme { + const QuillIconTheme({ + this.iconSelectedColor, + this.iconUnselectedColor, + this.iconSelectedFillColor, + this.iconUnselectedFillColor, + this.disabledIconColor, + this.disabledIconFillColor, + }); + + ///The color to use for selected icons in the toolbar + final Color? iconSelectedColor; + + ///The color to use for unselected icons in the toolbar + final Color? iconUnselectedColor; + + ///The fill color to use for the selected icons in the toolbar + final Color? iconSelectedFillColor; + + ///The fill color to use for the unselected icons in the toolbar + final Color? iconUnselectedFillColor; + + ///The color to use for disabled icons in the toolbar + final Color? disabledIconColor; + + ///The fill color to use for disabled icons in the toolbar + final Color? disabledIconFillColor; +} diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index d287e65c..8a1e2b79 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -258,16 +258,19 @@ class QuillEditor extends StatefulWidget { factory QuillEditor.basic({ required QuillController controller, required bool readOnly, + Brightness? keyboardAppearance, }) { return QuillEditor( - controller: controller, - scrollController: ScrollController(), - scrollable: true, - focusNode: FocusNode(), - autoFocus: true, - readOnly: readOnly, - expands: false, - padding: EdgeInsets.zero); + controller: controller, + scrollController: ScrollController(), + scrollable: true, + focusNode: FocusNode(), + autoFocus: true, + readOnly: readOnly, + expands: false, + padding: EdgeInsets.zero, + keyboardAppearance: keyboardAppearance ?? Brightness.light, + ); } final QuillController controller; diff --git a/lib/src/widgets/link_dialog.dart b/lib/src/widgets/link_dialog.dart index 3f9d7feb..9df89e69 100644 --- a/lib/src/widgets/link_dialog.dart +++ b/lib/src/widgets/link_dialog.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; +import '../models/themes/quill_dialog_theme.dart'; class LinkDialog extends StatefulWidget { - const LinkDialog({Key? key}) : super(key: key); + const LinkDialog({this.dialogTheme, Key? key}) : super(key: key); + + final QuillDialogTheme? dialogTheme; @override LinkDialogState createState() => LinkDialogState(); @@ -13,15 +16,23 @@ class LinkDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( + backgroundColor: widget.dialogTheme?.dialogBackgroundColor, content: TextField( - decoration: const InputDecoration(labelText: 'Paste a link'), + style: widget.dialogTheme?.inputTextStyle, + decoration: InputDecoration( + labelText: 'Paste a link', + labelStyle: widget.dialogTheme?.labelTextStyle, + floatingLabelStyle: widget.dialogTheme?.labelTextStyle), autofocus: true, onChanged: _linkChanged, ), actions: [ TextButton( onPressed: _link.isNotEmpty ? _applyLink : null, - child: const Text('Ok'), + child: Text( + 'Ok', + style: widget.dialogTheme?.labelTextStyle, + ), ), ], ); diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 1fd6b0f1..ac1c4070 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -3,6 +3,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; import '../models/documents/attribute.dart'; +import '../models/themes/quill_icon_theme.dart'; +import '../models/themes/quill_dialog_theme.dart'; import '../utils/media_pick_setting.dart'; import 'controller.dart'; import 'toolbar/arrow_indicated_button_list.dart'; @@ -95,6 +97,12 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { FilePickImpl? filePickImpl, WebImagePickImpl? webImagePickImpl, WebVideoPickImpl? webVideoPickImpl, + + ///The theme to use for the icons in the toolbar, uses type [QuillIconTheme] + QuillIconTheme? iconTheme, + + ///The theme to use for the theming of the [LinkDialog()], shown when embedding an image, for example + QuillDialogTheme? dialogTheme, Key? key, }) { final isButtonGroupShown = [ @@ -128,6 +136,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { iconSize: toolbarIconSize, controller: controller, undo: true, + iconTheme: iconTheme, ), if (showHistory) HistoryButton( @@ -135,6 +144,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { iconSize: toolbarIconSize, controller: controller, undo: false, + iconTheme: iconTheme, ), if (showBoldButton) ToggleStyleButton( @@ -142,6 +152,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { icon: Icons.format_bold, iconSize: toolbarIconSize, controller: controller, + iconTheme: iconTheme, ), if (showItalicButton) ToggleStyleButton( @@ -149,6 +160,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { icon: Icons.format_italic, iconSize: toolbarIconSize, controller: controller, + iconTheme: iconTheme, ), if (showSmallButton) ToggleStyleButton( @@ -156,6 +168,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { icon: Icons.format_size, iconSize: toolbarIconSize, controller: controller, + iconTheme: iconTheme, ), if (showUnderLineButton) ToggleStyleButton( @@ -163,6 +176,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { icon: Icons.format_underline, iconSize: toolbarIconSize, controller: controller, + iconTheme: iconTheme, ), if (showStrikeThrough) ToggleStyleButton( @@ -170,6 +184,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { icon: Icons.format_strikethrough, iconSize: toolbarIconSize, controller: controller, + iconTheme: iconTheme, ), if (showInlineCode) ToggleStyleButton( @@ -177,6 +192,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { icon: Icons.code, iconSize: toolbarIconSize, controller: controller, + iconTheme: iconTheme, ), if (showColorButton) ColorButton( @@ -184,6 +200,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { iconSize: toolbarIconSize, controller: controller, background: false, + iconTheme: iconTheme, ), if (showBackgroundColorButton) ColorButton( @@ -191,12 +208,14 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { iconSize: toolbarIconSize, controller: controller, background: true, + iconTheme: iconTheme, ), if (showClearFormat) ClearFormatButton( icon: Icons.format_clear, iconSize: toolbarIconSize, controller: controller, + iconTheme: iconTheme, ), if (showImageButton) ImageButton( @@ -207,6 +226,8 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { filePickImpl: filePickImpl, webImagePickImpl: webImagePickImpl, mediaPickSettingSelector: mediaPickSettingSelector, + iconTheme: iconTheme, + dialogTheme: dialogTheme, ), if (showVideoButton) VideoButton( @@ -217,18 +238,22 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { filePickImpl: filePickImpl, webVideoPickImpl: webImagePickImpl, mediaPickSettingSelector: mediaPickSettingSelector, + iconTheme: iconTheme, + dialogTheme: dialogTheme, ), if ((onImagePickCallback != null || onVideoPickCallback != null) && showCameraButton) CameraButton( - icon: Icons.photo_camera, - iconSize: toolbarIconSize, - controller: controller, - onImagePickCallback: onImagePickCallback, - onVideoPickCallback: onVideoPickCallback, - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl, - webVideoPickImpl: webVideoPickImpl), + icon: Icons.photo_camera, + iconSize: toolbarIconSize, + controller: controller, + onImagePickCallback: onImagePickCallback, + onVideoPickCallback: onVideoPickCallback, + filePickImpl: filePickImpl, + webImagePickImpl: webImagePickImpl, + webVideoPickImpl: webVideoPickImpl, + iconTheme: iconTheme, + ), if (isButtonGroupShown[0] && (isButtonGroupShown[1] || isButtonGroupShown[2] || @@ -259,6 +284,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { SelectHeaderStyleButton( controller: controller, iconSize: toolbarIconSize, + iconTheme: iconTheme, ), if (isButtonGroupShown[2] && (isButtonGroupShown[3] || @@ -275,6 +301,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { controller: controller, icon: Icons.format_list_numbered, iconSize: toolbarIconSize, + iconTheme: iconTheme, ), if (showListBullets) ToggleStyleButton( @@ -282,6 +309,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { controller: controller, icon: Icons.format_list_bulleted, iconSize: toolbarIconSize, + iconTheme: iconTheme, ), if (showListCheck) ToggleCheckListButton( @@ -289,6 +317,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { controller: controller, icon: Icons.check_box, iconSize: toolbarIconSize, + iconTheme: iconTheme, ), if (showCodeBlock) ToggleStyleButton( @@ -296,6 +325,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { controller: controller, icon: Icons.code, iconSize: toolbarIconSize, + iconTheme: iconTheme, ), if (isButtonGroupShown[3] && (isButtonGroupShown[4] || isButtonGroupShown[5])) @@ -310,6 +340,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { controller: controller, icon: Icons.format_quote, iconSize: toolbarIconSize, + iconTheme: iconTheme, ), if (showIndent) IndentButton( @@ -317,6 +348,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { iconSize: toolbarIconSize, controller: controller, isIncrease: true, + iconTheme: iconTheme, ), if (showIndent) IndentButton( @@ -324,6 +356,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { iconSize: toolbarIconSize, controller: controller, isIncrease: false, + iconTheme: iconTheme, ), if (isButtonGroupShown[4] && isButtonGroupShown[5]) VerticalDivider( @@ -335,12 +368,15 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { LinkStyleButton( controller: controller, iconSize: toolbarIconSize, + iconTheme: iconTheme, + dialogTheme: dialogTheme, ), if (showHorizontalRule) InsertEmbedButton( controller: controller, icon: Icons.horizontal_rule, iconSize: toolbarIconSize, + iconTheme: iconTheme, ), ], ); diff --git a/lib/src/widgets/toolbar/camera_button.dart b/lib/src/widgets/toolbar/camera_button.dart index 9c4f2d0d..9810bc84 100644 --- a/lib/src/widgets/toolbar/camera_button.dart +++ b/lib/src/widgets/toolbar/camera_button.dart @@ -4,6 +4,7 @@ import 'package:image_picker/image_picker.dart'; import '../controller.dart'; import '../toolbar.dart'; +import '../../models/themes/quill_icon_theme.dart'; import 'image_video_utils.dart'; import 'quill_icon_button.dart'; @@ -18,6 +19,7 @@ class CameraButton extends StatelessWidget { this.filePickImpl, this.webImagePickImpl, this.webVideoPickImpl, + this.iconTheme, Key? key, }) : super(key: key); @@ -38,16 +40,22 @@ class CameraButton extends StatelessWidget { final FilePickImpl? filePickImpl; + final QuillIconTheme? iconTheme; + @override Widget build(BuildContext context) { final theme = Theme.of(context); + final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + final iconFillColor = + iconTheme?.iconUnselectedFillColor ?? (fillColor ?? theme.canvasColor); + return QuillIconButton( - icon: Icon(icon, size: iconSize, color: theme.iconTheme.color), + icon: Icon(icon, size: iconSize, color: iconColor), highlightElevation: 0, hoverElevation: 0, size: iconSize * 1.77, - fillColor: fillColor ?? theme.canvasColor, + fillColor: iconFillColor, onPressed: () => _handleCameraButtonTap(context, controller, onImagePickCallback: onImagePickCallback, onVideoPickCallback: onVideoPickCallback, diff --git a/lib/src/widgets/toolbar/clear_format_button.dart b/lib/src/widgets/toolbar/clear_format_button.dart index 67f1e8d2..7410c637 100644 --- a/lib/src/widgets/toolbar/clear_format_button.dart +++ b/lib/src/widgets/toolbar/clear_format_button.dart @@ -8,6 +8,7 @@ class ClearFormatButton extends StatefulWidget { required this.icon, required this.controller, this.iconSize = kDefaultIconSize, + this.iconTheme, Key? key, }) : super(key: key); @@ -16,6 +17,8 @@ class ClearFormatButton extends StatefulWidget { final QuillController controller; + final QuillIconTheme? iconTheme; + @override _ClearFormatButtonState createState() => _ClearFormatButtonState(); } @@ -24,8 +27,10 @@ class _ClearFormatButtonState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final iconColor = theme.iconTheme.color; - final fillColor = theme.canvasColor; + final iconColor = + widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + final fillColor = + widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor; return QuillIconButton( highlightElevation: 0, hoverElevation: 0, diff --git a/lib/src/widgets/toolbar/color_button.dart b/lib/src/widgets/toolbar/color_button.dart index fa757e8a..59bd3bcd 100644 --- a/lib/src/widgets/toolbar/color_button.dart +++ b/lib/src/widgets/toolbar/color_button.dart @@ -3,6 +3,7 @@ import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import '../../models/documents/attribute.dart'; import '../../models/documents/style.dart'; +import '../../models/themes/quill_icon_theme.dart'; import '../../utils/color.dart'; import '../controller.dart'; import '../toolbar.dart'; @@ -18,6 +19,7 @@ class ColorButton extends StatefulWidget { required this.controller, required this.background, this.iconSize = kDefaultIconSize, + this.iconTheme, Key? key, }) : super(key: key); @@ -25,6 +27,7 @@ class ColorButton extends StatefulWidget { final double iconSize; final bool background; final QuillController controller; + final QuillIconTheme? iconTheme; @override _ColorButtonState createState() => _ColorButtonState(); @@ -98,20 +101,20 @@ class _ColorButtonState extends State { final theme = Theme.of(context); final iconColor = _isToggledColor && !widget.background && !_isWhite ? stringToColor(_selectionStyle.attributes['color']!.value) - : theme.iconTheme.color; + : (widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color); final iconColorBackground = _isToggledBackground && widget.background && !_isWhitebackground ? stringToColor(_selectionStyle.attributes['background']!.value) - : theme.iconTheme.color; + : (widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color); final fillColor = _isToggledColor && !widget.background && _isWhite ? stringToColor('#ffffff') - : theme.canvasColor; + : (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor); final fillColorBackground = _isToggledBackground && widget.background && _isWhitebackground ? stringToColor('#ffffff') - : theme.canvasColor; + : (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor); return QuillIconButton( highlightElevation: 0, diff --git a/lib/src/widgets/toolbar/history_button.dart b/lib/src/widgets/toolbar/history_button.dart index 2ed794c5..f9a8bf93 100644 --- a/lib/src/widgets/toolbar/history_button.dart +++ b/lib/src/widgets/toolbar/history_button.dart @@ -9,6 +9,7 @@ class HistoryButton extends StatefulWidget { required this.controller, required this.undo, this.iconSize = kDefaultIconSize, + this.iconTheme, Key? key, }) : super(key: key); @@ -16,6 +17,7 @@ class HistoryButton extends StatefulWidget { final double iconSize; final bool undo; final QuillController controller; + final QuillIconTheme? iconTheme; @override _HistoryButtonState createState() => _HistoryButtonState(); @@ -30,7 +32,8 @@ class _HistoryButtonState extends State { theme = Theme.of(context); _setIconColor(); - final fillColor = theme.canvasColor; + final fillColor = + widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor; widget.controller.changes.listen((event) async { _setIconColor(); }); @@ -50,14 +53,14 @@ class _HistoryButtonState extends State { if (widget.undo) { setState(() { _iconColor = widget.controller.hasUndo - ? theme.iconTheme.color - : theme.disabledColor; + ? widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color + : widget.iconTheme?.disabledIconColor ?? theme.disabledColor; }); } else { setState(() { _iconColor = widget.controller.hasRedo - ? theme.iconTheme.color - : theme.disabledColor; + ? widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color + : widget.iconTheme?.disabledIconColor ?? theme.disabledColor; }); } } diff --git a/lib/src/widgets/toolbar/image_button.dart b/lib/src/widgets/toolbar/image_button.dart index d30078f0..09616e7c 100644 --- a/lib/src/widgets/toolbar/image_button.dart +++ b/lib/src/widgets/toolbar/image_button.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import '../../models/documents/nodes/embed.dart'; +import '../../models/themes/quill_dialog_theme.dart'; +import '../../models/themes/quill_icon_theme.dart'; import '../../utils/media_pick_setting.dart'; import '../controller.dart'; import '../link_dialog.dart'; @@ -19,6 +21,8 @@ class ImageButton extends StatelessWidget { this.filePickImpl, this.webImagePickImpl, this.mediaPickSettingSelector, + this.iconTheme, + this.dialogTheme, Key? key, }) : super(key: key); @@ -37,16 +41,24 @@ class ImageButton extends StatelessWidget { final MediaPickSettingSelector? mediaPickSettingSelector; + final QuillIconTheme? iconTheme; + + final QuillDialogTheme? dialogTheme; + @override Widget build(BuildContext context) { final theme = Theme.of(context); + final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + final iconFillColor = + iconTheme?.iconUnselectedFillColor ?? (fillColor ?? theme.canvasColor); + return QuillIconButton( - icon: Icon(icon, size: iconSize, color: theme.iconTheme.color), + icon: Icon(icon, size: iconSize, color: iconColor), highlightElevation: 0, hoverElevation: 0, size: iconSize * 1.77, - fillColor: fillColor ?? theme.canvasColor, + fillColor: iconFillColor, onPressed: () => _onPressedHandler(context), ); } @@ -80,7 +92,7 @@ class ImageButton extends StatelessWidget { void _typeLink(BuildContext context) { showDialog( context: context, - builder: (_) => const LinkDialog(), + builder: (_) => LinkDialog(dialogTheme: dialogTheme), ).then(_linkSubmitted); } diff --git a/lib/src/widgets/toolbar/indent_button.dart b/lib/src/widgets/toolbar/indent_button.dart index aa6dfadb..d72718fc 100644 --- a/lib/src/widgets/toolbar/indent_button.dart +++ b/lib/src/widgets/toolbar/indent_button.dart @@ -9,6 +9,7 @@ class IndentButton extends StatefulWidget { required this.controller, required this.isIncrease, this.iconSize = kDefaultIconSize, + this.iconTheme, Key? key, }) : super(key: key); @@ -17,6 +18,8 @@ class IndentButton extends StatefulWidget { final QuillController controller; final bool isIncrease; + final QuillIconTheme? iconTheme; + @override _IndentButtonState createState() => _IndentButtonState(); } @@ -25,14 +28,17 @@ class _IndentButtonState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final iconColor = theme.iconTheme.color; - final fillColor = theme.canvasColor; + + final iconColor = + widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + final iconFillColor = + widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor; return QuillIconButton( highlightElevation: 0, hoverElevation: 0, size: widget.iconSize * 1.77, icon: Icon(widget.icon, size: widget.iconSize, color: iconColor), - fillColor: fillColor, + fillColor: iconFillColor, onPressed: () { final indent = widget.controller .getSelectionStyle() diff --git a/lib/src/widgets/toolbar/insert_embed_button.dart b/lib/src/widgets/toolbar/insert_embed_button.dart index 5c889b69..91a6822c 100644 --- a/lib/src/widgets/toolbar/insert_embed_button.dart +++ b/lib/src/widgets/toolbar/insert_embed_button.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../../models/documents/nodes/embed.dart'; +import '../../models/themes/quill_icon_theme.dart'; import '../controller.dart'; import '../toolbar.dart'; import 'quill_icon_button.dart'; @@ -11,6 +12,7 @@ class InsertEmbedButton extends StatelessWidget { required this.icon, this.iconSize = kDefaultIconSize, this.fillColor, + this.iconTheme, Key? key, }) : super(key: key); @@ -18,9 +20,16 @@ class InsertEmbedButton extends StatelessWidget { final IconData icon; final double iconSize; final Color? fillColor; + final QuillIconTheme? iconTheme; @override Widget build(BuildContext context) { + final theme = Theme.of(context); + + final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + final iconFillColor = + iconTheme?.iconUnselectedFillColor ?? (fillColor ?? theme.canvasColor); + return QuillIconButton( highlightElevation: 0, hoverElevation: 0, @@ -28,9 +37,9 @@ class InsertEmbedButton extends StatelessWidget { icon: Icon( icon, size: iconSize, - color: Theme.of(context).iconTheme.color, + color: iconColor, ), - fillColor: fillColor ?? Theme.of(context).canvasColor, + fillColor: iconFillColor, onPressed: () { final index = controller.selection.baseOffset; final length = controller.selection.extentOffset - index; diff --git a/lib/src/widgets/toolbar/link_style_button.dart b/lib/src/widgets/toolbar/link_style_button.dart index 32210504..f6bc975f 100644 --- a/lib/src/widgets/toolbar/link_style_button.dart +++ b/lib/src/widgets/toolbar/link_style_button.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import '../../models/documents/attribute.dart'; +import '../../models/themes/quill_dialog_theme.dart'; +import '../../models/themes/quill_icon_theme.dart'; import '../controller.dart'; import '../link_dialog.dart'; import '../toolbar.dart'; @@ -11,12 +13,16 @@ class LinkStyleButton extends StatefulWidget { required this.controller, this.iconSize = kDefaultIconSize, this.icon, + this.iconTheme, + this.dialogTheme, Key? key, }) : super(key: key); final QuillController controller; final IconData? icon; final double iconSize; + final QuillIconTheme? iconTheme; + final QuillDialogTheme? dialogTheme; @override _LinkStyleButtonState createState() => _LinkStyleButtonState(); @@ -60,9 +66,11 @@ class _LinkStyleButtonState extends State { icon: Icon( widget.icon ?? Icons.link, size: widget.iconSize, - color: isEnabled ? theme.iconTheme.color : theme.disabledColor, + color: isEnabled + ? (widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color) + : (widget.iconTheme?.disabledIconColor ?? theme.disabledColor), ), - fillColor: Theme.of(context).canvasColor, + fillColor: widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor, onPressed: pressedHandler, ); } @@ -71,7 +79,7 @@ class _LinkStyleButtonState extends State { showDialog( context: context, builder: (ctx) { - return const LinkDialog(); + return LinkDialog(dialogTheme: widget.dialogTheme); }, ).then(_linkSubmitted); } diff --git a/lib/src/widgets/toolbar/select_alignment_button.dart b/lib/src/widgets/toolbar/select_alignment_button.dart index 37e5e72e..9761a96d 100644 --- a/lib/src/widgets/toolbar/select_alignment_button.dart +++ b/lib/src/widgets/toolbar/select_alignment_button.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../../models/documents/attribute.dart'; import '../../models/documents/style.dart'; +import '../../models/themes/quill_icon_theme.dart'; import '../controller.dart'; import '../toolbar.dart'; @@ -10,12 +11,15 @@ class SelectAlignmentButton extends StatefulWidget { const SelectAlignmentButton({ required this.controller, this.iconSize = kDefaultIconSize, + this.iconTheme, Key? key, }) : super(key: key); final QuillController controller; final double iconSize; + final QuillIconTheme? iconTheme; + @override _SelectAlignmentButtonState createState() => _SelectAlignmentButtonState(); } @@ -77,8 +81,10 @@ class _SelectAlignmentButtonState extends State { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(2)), fillColor: _valueToText[_value] == _valueString[index] - ? theme.toggleableActiveColor - : theme.canvasColor, + ? (widget.iconTheme?.iconSelectedFillColor ?? + theme.toggleableActiveColor) + : (widget.iconTheme?.iconUnselectedFillColor ?? + theme.canvasColor), onPressed: () => _valueAttribute[index] == Attribute.leftAlignment ? widget.controller .formatSelection(Attribute.clone(Attribute.align, null)) @@ -93,8 +99,10 @@ class _SelectAlignmentButtonState extends State { : Icons.format_align_justify, size: widget.iconSize, color: _valueToText[_value] == _valueString[index] - ? theme.primaryIconTheme.color - : theme.iconTheme.color, + ? (widget.iconTheme?.iconSelectedColor ?? + theme.primaryIconTheme.color) + : (widget.iconTheme?.iconUnselectedColor ?? + theme.iconTheme.color), ), ), ), diff --git a/lib/src/widgets/toolbar/select_header_style_button.dart b/lib/src/widgets/toolbar/select_header_style_button.dart index 715e3632..bb6eee65 100644 --- a/lib/src/widgets/toolbar/select_header_style_button.dart +++ b/lib/src/widgets/toolbar/select_header_style_button.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../../models/documents/attribute.dart'; import '../../models/documents/style.dart'; +import '../../models/themes/quill_icon_theme.dart'; import '../controller.dart'; import '../toolbar.dart'; @@ -10,12 +11,15 @@ class SelectHeaderStyleButton extends StatefulWidget { const SelectHeaderStyleButton({ required this.controller, this.iconSize = kDefaultIconSize, + this.iconTheme, Key? key, }) : super(key: key); final QuillController controller; final double iconSize; + final QuillIconTheme? iconTheme; + @override _SelectHeaderStyleButtonState createState() => _SelectHeaderStyleButtonState(); @@ -77,16 +81,20 @@ class _SelectHeaderStyleButtonState extends State { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(2)), fillColor: _valueToText[_value] == _valueString[index] - ? theme.toggleableActiveColor - : theme.canvasColor, + ? (widget.iconTheme?.iconSelectedFillColor ?? + theme.toggleableActiveColor) + : (widget.iconTheme?.iconUnselectedFillColor ?? + theme.canvasColor), onPressed: () => widget.controller.formatSelection(_valueAttribute[index]), child: Text( _valueString[index], style: style.copyWith( color: _valueToText[_value] == _valueString[index] - ? theme.primaryIconTheme.color - : theme.iconTheme.color, + ? (widget.iconTheme?.iconSelectedColor ?? + theme.primaryIconTheme.color) + : (widget.iconTheme?.iconUnselectedColor ?? + theme.iconTheme.color), ), ), ), diff --git a/lib/src/widgets/toolbar/toggle_check_list_button.dart b/lib/src/widgets/toolbar/toggle_check_list_button.dart index c147e108..e631d23a 100644 --- a/lib/src/widgets/toolbar/toggle_check_list_button.dart +++ b/lib/src/widgets/toolbar/toggle_check_list_button.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../../models/documents/attribute.dart'; import '../../models/documents/style.dart'; +import '../../models/themes/quill_icon_theme.dart'; import '../controller.dart'; import '../toolbar.dart'; import 'toggle_style_button.dart'; @@ -14,6 +15,7 @@ class ToggleCheckListButton extends StatefulWidget { this.iconSize = kDefaultIconSize, this.fillColor, this.childBuilder = defaultToggleStyleButtonBuilder, + this.iconTheme, Key? key, }) : super(key: key); @@ -28,6 +30,8 @@ class ToggleCheckListButton extends StatefulWidget { final Attribute attribute; + final QuillIconTheme? iconTheme; + @override _ToggleCheckListButtonState createState() => _ToggleCheckListButtonState(); } @@ -89,6 +93,7 @@ class _ToggleCheckListButtonState extends State { _isToggled, _toggleAttribute, widget.iconSize, + widget.iconTheme, ); } diff --git a/lib/src/widgets/toolbar/toggle_style_button.dart b/lib/src/widgets/toolbar/toggle_style_button.dart index 8299f8a4..caf36879 100644 --- a/lib/src/widgets/toolbar/toggle_style_button.dart +++ b/lib/src/widgets/toolbar/toggle_style_button.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../../models/documents/attribute.dart'; import '../../models/documents/style.dart'; +import '../../models/themes/quill_icon_theme.dart'; import '../controller.dart'; import '../toolbar.dart'; import 'quill_icon_button.dart'; @@ -14,6 +15,7 @@ typedef ToggleStyleButtonBuilder = Widget Function( bool? isToggled, VoidCallback? onPressed, [ double iconSize, + QuillIconTheme? iconTheme, ]); class ToggleStyleButton extends StatefulWidget { @@ -24,6 +26,7 @@ class ToggleStyleButton extends StatefulWidget { this.iconSize = kDefaultIconSize, this.fillColor, this.childBuilder = defaultToggleStyleButtonBuilder, + this.iconTheme, Key? key, }) : super(key: key); @@ -38,6 +41,9 @@ class ToggleStyleButton extends StatefulWidget { final ToggleStyleButtonBuilder childBuilder; + ///Specify an icon theme for the icons in the toolbar + final QuillIconTheme? iconTheme; + @override _ToggleStyleButtonState createState() => _ToggleStyleButtonState(); } @@ -64,6 +70,7 @@ class _ToggleStyleButtonState extends State { _isToggled, _toggleAttribute, widget.iconSize, + widget.iconTheme, ); } @@ -113,17 +120,25 @@ Widget defaultToggleStyleButtonBuilder( bool? isToggled, VoidCallback? onPressed, [ double iconSize = kDefaultIconSize, + QuillIconTheme? iconTheme, ]) { final theme = Theme.of(context); final isEnabled = onPressed != null; final iconColor = isEnabled ? isToggled == true - ? theme.primaryIconTheme.color - : theme.iconTheme.color - : theme.disabledColor; - final fill = isToggled == true - ? theme.toggleableActiveColor - : fillColor ?? theme.canvasColor; + ? (iconTheme?.iconSelectedColor ?? + theme + .primaryIconTheme.color) //You can specify your own icon color + : (iconTheme?.iconUnselectedColor ?? theme.iconTheme.color) + : (iconTheme?.disabledIconColor ?? theme.disabledColor); + final fill = isEnabled + ? isToggled == true + ? (iconTheme?.iconSelectedFillColor ?? + theme.toggleableActiveColor) //Selected icon fill color + : (iconTheme?.iconUnselectedFillColor ?? + theme.canvasColor) //Unselected icon fill color : + : (iconTheme?.disabledIconFillColor ?? + (fillColor ?? theme.canvasColor)); //Disabled icon fill color return QuillIconButton( highlightElevation: 0, hoverElevation: 0, diff --git a/lib/src/widgets/toolbar/video_button.dart b/lib/src/widgets/toolbar/video_button.dart index 8f2c797d..db2afdeb 100644 --- a/lib/src/widgets/toolbar/video_button.dart +++ b/lib/src/widgets/toolbar/video_button.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import '../../models/documents/nodes/embed.dart'; +import '../../models/themes/quill_dialog_theme.dart'; +import '../../models/themes/quill_icon_theme.dart'; import '../../utils/media_pick_setting.dart'; import '../controller.dart'; import '../link_dialog.dart'; @@ -19,6 +21,8 @@ class VideoButton extends StatelessWidget { this.filePickImpl, this.webVideoPickImpl, this.mediaPickSettingSelector, + this.iconTheme, + this.dialogTheme, Key? key, }) : super(key: key); @@ -37,16 +41,24 @@ class VideoButton extends StatelessWidget { final MediaPickSettingSelector? mediaPickSettingSelector; + final QuillIconTheme? iconTheme; + + final QuillDialogTheme? dialogTheme; + @override Widget build(BuildContext context) { final theme = Theme.of(context); + final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + final iconFillColor = + iconTheme?.iconUnselectedFillColor ?? (fillColor ?? theme.canvasColor); + return QuillIconButton( - icon: Icon(icon, size: iconSize, color: theme.iconTheme.color), + icon: Icon(icon, size: iconSize, color: iconColor), highlightElevation: 0, hoverElevation: 0, size: iconSize * 1.77, - fillColor: fillColor ?? theme.canvasColor, + fillColor: iconFillColor, onPressed: () => _onPressedHandler(context), ); } @@ -80,7 +92,7 @@ class VideoButton extends StatelessWidget { void _typeLink(BuildContext context) { showDialog( context: context, - builder: (_) => const LinkDialog(), + builder: (_) => LinkDialog(dialogTheme: dialogTheme), ).then(_linkSubmitted); } From aeb8da7d03a83a04324c255e61c3236f43aa7706 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sat, 16 Oct 2021 20:38:32 -0700 Subject: [PATCH 030/179] Upgrade to 2.0.7 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf25e556..be9a8eca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.7] +* Added theming options for toolbar icons and LinkDialog. + ## [2.0.6] * Avoid runtime error when placed inside TabBarView. diff --git a/pubspec.yaml b/pubspec.yaml index 4216d878..197c0c4b 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: 2.0.6 +version: 2.0.7 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 04d132247c478103ef6fc31166be95ebc4057731 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sat, 16 Oct 2021 20:44:58 -0700 Subject: [PATCH 031/179] Fix lint complaints --- lib/src/widgets/toolbar.dart | 5 +++-- lib/src/widgets/toolbar/camera_button.dart | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index ac1c4070..586b7dee 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -3,8 +3,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; import '../models/documents/attribute.dart'; -import '../models/themes/quill_icon_theme.dart'; import '../models/themes/quill_dialog_theme.dart'; +import '../models/themes/quill_icon_theme.dart'; import '../utils/media_pick_setting.dart'; import 'controller.dart'; import 'toolbar/arrow_indicated_button_list.dart'; @@ -101,7 +101,8 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { ///The theme to use for the icons in the toolbar, uses type [QuillIconTheme] QuillIconTheme? iconTheme, - ///The theme to use for the theming of the [LinkDialog()], shown when embedding an image, for example + ///The theme to use for the theming of the [LinkDialog()], + ///shown when embedding an image, for example QuillDialogTheme? dialogTheme, Key? key, }) { diff --git a/lib/src/widgets/toolbar/camera_button.dart b/lib/src/widgets/toolbar/camera_button.dart index 9810bc84..7686e47c 100644 --- a/lib/src/widgets/toolbar/camera_button.dart +++ b/lib/src/widgets/toolbar/camera_button.dart @@ -2,9 +2,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; +import '../../models/themes/quill_icon_theme.dart'; import '../controller.dart'; import '../toolbar.dart'; -import '../../models/themes/quill_icon_theme.dart'; import 'image_video_utils.dart'; import 'quill_icon_button.dart'; From e81ef95b6a0f4eb197f87d4af7a21b2eca550941 Mon Sep 17 00:00:00 2001 From: li3317 Date: Sun, 17 Oct 2021 10:05:21 -0400 Subject: [PATCH 032/179] upgrade library --- pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 197c0c4b..430e2076 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,10 +13,10 @@ dependencies: flutter: sdk: flutter collection: ^1.15.0 - flutter_colorpicker: ^0.5.0 + flutter_colorpicker: ^0.6.0 flutter_keyboard_visibility: ^5.0.0 image_picker: ^0.8.2 - photo_view: ^0.12.0 + photo_view: ^0.13.0 quiver: ^3.0.0 string_validator: ^0.3.0 tuple: ^2.0.0 From bc7469643a51e420975375c67dd46200691021a0 Mon Sep 17 00:00:00 2001 From: Namli1 <51393783+Namli1@users.noreply.github.com> Date: Mon, 18 Oct 2021 00:06:44 +0200 Subject: [PATCH 033/179] Adding translations to the toolbar! (#421) --- README.md | 14 ++++++ lib/src/translations/toolbar.i18n.dart | 37 +++++++++++++++ lib/src/widgets/link_dialog.dart | 6 ++- lib/src/widgets/toolbar.dart | 46 ++++++++++++++----- lib/src/widgets/toolbar/color_button.dart | 3 +- .../widgets/toolbar/image_video_utils.dart | 5 +- pubspec.yaml | 1 + 7 files changed, 95 insertions(+), 17 deletions(-) create mode 100644 lib/src/translations/toolbar.i18n.dart diff --git a/README.md b/README.md index 4d5a2756..8386a922 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,20 @@ Define `mobileWidth`, `mobileHeight`, `mobileMargin`, `mobileAlignment` as follo } ``` +## Translation of toolbar +The package offers translations for the quill toolbar, it will follow the system locale unless you set your own locale with: +``` +QuillToolbar(locale: Locale('fr'), ...) +``` +Currently, translations are available for these locales: +* `Locale('en')` +* `Locale('de')` +* `Locale('fr')` +* `Locale('zh', 'CN')` + +### Contributing to translations +The translation file is located at [lib/src/translations/toolbar.i18n.dart](lib/src/translations/toolbar.i18n.dart). Feel free to contribute your own translations, just copy the English translations map and replace the values with your translations. Then open a pull request so everyone can benefit from your translations! + ## Migrate Zefyr Data Check out [code](https://github.com/jwehrle/zefyr_quill_convert) and [doc](https://docs.google.com/document/d/1FUSrpbarHnilb7uDN5J5DDahaI0v1RMXBjj4fFSpSuY/edit?usp=sharing). diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart new file mode 100644 index 00000000..137f6ee4 --- /dev/null +++ b/lib/src/translations/toolbar.i18n.dart @@ -0,0 +1,37 @@ +import 'package:i18n_extension/i18n_extension.dart'; + +extension Localization on String { + static final _t = Translations.byLocale('en') + + { + 'en': { + 'Paste a link': 'Paste a link', + 'Ok': 'Ok', + 'Select Color': 'Select Color', + 'Gallery': 'Gallery', + 'Link': 'Link', + }, + 'de': { + 'Paste a link': 'Link hinzufügen', + 'Ok': 'Ok', + 'Select Color': 'Farbe auswählen', + 'Gallery': 'Gallerie', + 'Link': 'Link', + }, + 'fr': { + 'Paste a link': 'Coller un lien', + 'Ok': 'Ok', + 'Select Color': 'Choisir une couleur', + 'Gallery': 'Galerie', + 'Link': 'Lien', + }, + 'zh_CN': { + 'Paste a link': '粘贴链接', + 'Ok': '好', + 'Select Color': '选择颜色', + 'Gallery': '相簿', + 'Link': '链接', + } + }; + + String get i18n => localize(this, _t); +} diff --git a/lib/src/widgets/link_dialog.dart b/lib/src/widgets/link_dialog.dart index 9df89e69..e6173402 100644 --- a/lib/src/widgets/link_dialog.dart +++ b/lib/src/widgets/link_dialog.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; + import '../models/themes/quill_dialog_theme.dart'; +import '../translations/toolbar.i18n.dart'; class LinkDialog extends StatefulWidget { const LinkDialog({this.dialogTheme, Key? key}) : super(key: key); @@ -20,7 +22,7 @@ class LinkDialogState extends State { content: TextField( style: widget.dialogTheme?.inputTextStyle, decoration: InputDecoration( - labelText: 'Paste a link', + labelText: 'Paste a link'.i18n, labelStyle: widget.dialogTheme?.labelTextStyle, floatingLabelStyle: widget.dialogTheme?.labelTextStyle), autofocus: true, @@ -30,7 +32,7 @@ class LinkDialogState extends State { TextButton( onPressed: _link.isNotEmpty ? _applyLink : null, child: Text( - 'Ok', + 'Ok'.i18n, style: widget.dialogTheme?.labelTextStyle, ), ), diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 586b7dee..a574d418 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:i18n_extension/i18n_widget.dart'; import '../models/documents/attribute.dart'; import '../models/themes/quill_dialog_theme.dart'; @@ -61,6 +62,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { this.color, this.filePickImpl, this.multiRowsDisplay, + this.locale, Key? key, }) : super(key: key); @@ -104,6 +106,14 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { ///The theme to use for the theming of the [LinkDialog()], ///shown when embedding an image, for example QuillDialogTheme? dialogTheme, + + ///The locale to use for the editor toolbar, defaults to system locale + ///Currently the supported locales are: + /// * Locale('en') + /// * Locale('de') + /// * Locale('fr') + /// * Locale('zh') + Locale? locale, Key? key, }) { final isButtonGroupShown = [ @@ -130,6 +140,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { key: key, toolBarHeight: toolbarIconSize * 2, multiRowsDisplay: multiRowsDisplay, + locale: locale, children: [ if (showHistory) HistoryButton( @@ -395,23 +406,34 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { final FilePickImpl? filePickImpl; + ///The locale to use for the editor toolbar, defaults to system locale + ///Currently the supported locales are: + /// * Locale('en') + /// * Locale('de') + /// * Locale('fr') + /// * Locale('zh', 'CN') + final Locale? locale; + @override Size get preferredSize => Size.fromHeight(toolBarHeight); @override Widget build(BuildContext context) { - if (multiRowsDisplay ?? true) { - return Wrap( - alignment: WrapAlignment.center, - runSpacing: 4, - spacing: 4, - children: children, - ); - } - return Container( - constraints: BoxConstraints.tightFor(height: preferredSize.height), - color: color ?? Theme.of(context).canvasColor, - child: ArrowIndicatedButtonList(buttons: children), + return I18n( + initialLocale: locale, + child: multiRowsDisplay ?? true + ? Wrap( + alignment: WrapAlignment.center, + runSpacing: 4, + spacing: 4, + children: children, + ) + : Container( + constraints: + BoxConstraints.tightFor(height: preferredSize.height), + color: color ?? Theme.of(context).canvasColor, + child: ArrowIndicatedButtonList(buttons: children), + ), ); } } diff --git a/lib/src/widgets/toolbar/color_button.dart b/lib/src/widgets/toolbar/color_button.dart index 59bd3bcd..c0d323bf 100644 --- a/lib/src/widgets/toolbar/color_button.dart +++ b/lib/src/widgets/toolbar/color_button.dart @@ -4,6 +4,7 @@ import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import '../../models/documents/attribute.dart'; import '../../models/documents/style.dart'; import '../../models/themes/quill_icon_theme.dart'; +import '../../translations/toolbar.i18n.dart'; import '../../utils/color.dart'; import '../controller.dart'; import '../toolbar.dart'; @@ -143,7 +144,7 @@ class _ColorButtonState extends State { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Select Color'), + title: Text('Select Color'.i18n), backgroundColor: Theme.of(context).canvasColor, content: SingleChildScrollView( child: MaterialPicker( diff --git a/lib/src/widgets/toolbar/image_video_utils.dart b/lib/src/widgets/toolbar/image_video_utils.dart index 0305378b..74820c7f 100644 --- a/lib/src/widgets/toolbar/image_video_utils.dart +++ b/lib/src/widgets/toolbar/image_video_utils.dart @@ -6,6 +6,7 @@ import 'package:image_picker/image_picker.dart'; import '../../models/documents/nodes/embed.dart'; import '../../utils/media_pick_setting.dart'; +import '../../translations/toolbar.i18n.dart'; import '../controller.dart'; import '../toolbar.dart'; @@ -26,7 +27,7 @@ class ImageVideoUtils { Icons.collections, color: Colors.orangeAccent, ), - label: const Text('Gallery'), + label: Text('Gallery'.i18n), onPressed: () => Navigator.pop(ctx, MediaPickSetting.Gallery), ), TextButton.icon( @@ -34,7 +35,7 @@ class ImageVideoUtils { Icons.link, color: Colors.cyanAccent, ), - label: const Text('Link'), + label: Text('Link'.i18n), onPressed: () => Navigator.pop(ctx, MediaPickSetting.Link), ) ], diff --git a/pubspec.yaml b/pubspec.yaml index 430e2076..c183026c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: characters: ^1.1.0 youtube_player_flutter: ^8.0.0 diff_match_patch: ^0.4.1 + i18n_extension: ^4.1.3 dev_dependencies: flutter_test: From 49b2cffda81c86cb92b711513d1eeea4067e0ead Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sun, 17 Oct 2021 16:08:03 -0700 Subject: [PATCH 034/179] Upgrade to 2.0.8 --- CHANGELOG.md | 3 +++ lib/src/widgets/toolbar/image_video_utils.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be9a8eca..61e90812 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.8] +* Adding translations to the toolbar. + ## [2.0.7] * Added theming options for toolbar icons and LinkDialog. diff --git a/lib/src/widgets/toolbar/image_video_utils.dart b/lib/src/widgets/toolbar/image_video_utils.dart index 74820c7f..c4591dd4 100644 --- a/lib/src/widgets/toolbar/image_video_utils.dart +++ b/lib/src/widgets/toolbar/image_video_utils.dart @@ -5,8 +5,8 @@ import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import '../../models/documents/nodes/embed.dart'; -import '../../utils/media_pick_setting.dart'; import '../../translations/toolbar.i18n.dart'; +import '../../utils/media_pick_setting.dart'; import '../controller.dart'; import '../toolbar.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index c183026c..84674dcd 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: 2.0.7 +version: 2.0.8 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 4cb9e7c167d5c57d84b55a0aac88618b4e19e1d7 Mon Sep 17 00:00:00 2001 From: Namli1 <51393783+Namli1@users.noreply.github.com> Date: Mon, 18 Oct 2021 22:34:40 +0200 Subject: [PATCH 035/179] Improve UX when trying to add a link (#422) --- lib/src/translations/toolbar.i18n.dart | 8 ++++ .../widgets/toolbar/image_video_utils.dart | 1 + .../widgets/toolbar/link_style_button.dart | 45 ++++++++++++++----- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 137f6ee4..39ee6377 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -9,6 +9,8 @@ extension Localization on String { 'Select Color': 'Select Color', 'Gallery': 'Gallery', 'Link': 'Link', + 'Please first select some text to transform into a link.': + 'Please first select some text to transform into a link.', }, 'de': { 'Paste a link': 'Link hinzufügen', @@ -16,6 +18,8 @@ extension Localization on String { 'Select Color': 'Farbe auswählen', 'Gallery': 'Gallerie', 'Link': 'Link', + 'Please first select some text to transform into a link.': + 'Markiere bitte zuerst einen Text, um diesen in einen Link zu verwandeln.', }, 'fr': { 'Paste a link': 'Coller un lien', @@ -23,6 +27,8 @@ extension Localization on String { 'Select Color': 'Choisir une couleur', 'Gallery': 'Galerie', 'Link': 'Lien', + 'Please first select some text to transform into a link.': + "Veuillez d'abord sélectionner un texte à transformer en lien.", }, 'zh_CN': { 'Paste a link': '粘贴链接', @@ -30,6 +36,8 @@ extension Localization on String { 'Select Color': '选择颜色', 'Gallery': '相簿', 'Link': '链接', + 'Please first select some text to transform into a link.': + '请先选择一些要转化为链接的文本', } }; diff --git a/lib/src/widgets/toolbar/image_video_utils.dart b/lib/src/widgets/toolbar/image_video_utils.dart index c4591dd4..449707a0 100644 --- a/lib/src/widgets/toolbar/image_video_utils.dart +++ b/lib/src/widgets/toolbar/image_video_utils.dart @@ -7,6 +7,7 @@ import 'package:image_picker/image_picker.dart'; import '../../models/documents/nodes/embed.dart'; import '../../translations/toolbar.i18n.dart'; import '../../utils/media_pick_setting.dart'; +import '../../translations/toolbar.i18n.dart'; import '../controller.dart'; import '../toolbar.dart'; diff --git a/lib/src/widgets/toolbar/link_style_button.dart b/lib/src/widgets/toolbar/link_style_button.dart index f6bc975f..a82366b1 100644 --- a/lib/src/widgets/toolbar/link_style_button.dart +++ b/lib/src/widgets/toolbar/link_style_button.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../../translations/toolbar.i18n.dart'; import '../../models/documents/attribute.dart'; import '../../models/themes/quill_dialog_theme.dart'; import '../../models/themes/quill_icon_theme.dart'; @@ -54,24 +55,44 @@ class _LinkStyleButtonState extends State { widget.controller.removeListener(_didChangeSelection); } + GlobalKey _toolTipKey = GlobalKey(); + @override Widget build(BuildContext context) { final theme = Theme.of(context); final isEnabled = !widget.controller.selection.isCollapsed; final pressedHandler = isEnabled ? () => _openLinkDialog(context) : null; - return QuillIconButton( - highlightElevation: 0, - hoverElevation: 0, - size: widget.iconSize * kIconButtonFactor, - icon: Icon( - widget.icon ?? Icons.link, - size: widget.iconSize, - color: isEnabled - ? (widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color) - : (widget.iconTheme?.disabledIconColor ?? theme.disabledColor), + return GestureDetector( + onTap: () async { + final dynamic tooltip = _toolTipKey.currentState; + tooltip.ensureTooltipVisible(); + Future.delayed( + const Duration( + seconds: 3, + ), + tooltip.deactivate, + ); + }, + child: Tooltip( + key: _toolTipKey, + message: 'Please first select some text to transform into a link.'.i18n, + child: QuillIconButton( + highlightElevation: 0, + hoverElevation: 0, + size: widget.iconSize * kIconButtonFactor, + icon: Icon( + widget.icon ?? Icons.link, + size: widget.iconSize, + color: isEnabled + ? (widget.iconTheme?.iconUnselectedColor ?? + theme.iconTheme.color) + : (widget.iconTheme?.disabledIconColor ?? theme.disabledColor), + ), + fillColor: + widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor, + onPressed: pressedHandler, + ), ), - fillColor: widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor, - onPressed: pressedHandler, ); } From a48e1f7bd2d821151ca6b71663995e67e9fbd15a Mon Sep 17 00:00:00 2001 From: li3317 Date: Mon, 18 Oct 2021 19:11:17 -0400 Subject: [PATCH 036/179] upgrade to 2.0.9 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61e90812..8c361b8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.9] +* Improve UX when trying to add a link. + ## [2.0.8] * Adding translations to the toolbar. From cf68700d2c943d1cbfe280d0ab03fb3d3dacdb13 Mon Sep 17 00:00:00 2001 From: li3317 Date: Mon, 18 Oct 2021 19:13:49 -0400 Subject: [PATCH 037/179] small change --- lib/src/translations/toolbar.i18n.dart | 3 ++- lib/src/widgets/toolbar/image_video_utils.dart | 1 - lib/src/widgets/toolbar/link_style_button.dart | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 39ee6377..a20abf36 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -19,7 +19,8 @@ extension Localization on String { 'Gallery': 'Gallerie', 'Link': 'Link', 'Please first select some text to transform into a link.': - 'Markiere bitte zuerst einen Text, um diesen in einen Link zu verwandeln.', + 'Markiere bitte zuerst einen Text, um diesen in einen Link zu ' + 'verwandeln.', }, 'fr': { 'Paste a link': 'Coller un lien', diff --git a/lib/src/widgets/toolbar/image_video_utils.dart b/lib/src/widgets/toolbar/image_video_utils.dart index 449707a0..c4591dd4 100644 --- a/lib/src/widgets/toolbar/image_video_utils.dart +++ b/lib/src/widgets/toolbar/image_video_utils.dart @@ -7,7 +7,6 @@ import 'package:image_picker/image_picker.dart'; import '../../models/documents/nodes/embed.dart'; import '../../translations/toolbar.i18n.dart'; import '../../utils/media_pick_setting.dart'; -import '../../translations/toolbar.i18n.dart'; import '../controller.dart'; import '../toolbar.dart'; diff --git a/lib/src/widgets/toolbar/link_style_button.dart b/lib/src/widgets/toolbar/link_style_button.dart index a82366b1..5ec66b29 100644 --- a/lib/src/widgets/toolbar/link_style_button.dart +++ b/lib/src/widgets/toolbar/link_style_button.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import '../../translations/toolbar.i18n.dart'; import '../../models/documents/attribute.dart'; import '../../models/themes/quill_dialog_theme.dart'; import '../../models/themes/quill_icon_theme.dart'; +import '../../translations/toolbar.i18n.dart'; import '../controller.dart'; import '../link_dialog.dart'; import '../toolbar.dart'; @@ -55,7 +55,7 @@ class _LinkStyleButtonState extends State { widget.controller.removeListener(_didChangeSelection); } - GlobalKey _toolTipKey = GlobalKey(); + final GlobalKey _toolTipKey = GlobalKey(); @override Widget build(BuildContext context) { From d1320c4236e3be4b6f8bd5af3fefdfe9e2ad14ed Mon Sep 17 00:00:00 2001 From: li3317 Date: Mon, 18 Oct 2021 19:38:14 -0400 Subject: [PATCH 038/179] upgrade to 2.0.9 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 84674dcd..355b0239 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: 2.0.8 +version: 2.0.9 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From aea3159ddc5a54c904c44c68d823469ca24da40d Mon Sep 17 00:00:00 2001 From: appflowy <86001920+appflowy@users.noreply.github.com> Date: Sat, 23 Oct 2021 23:05:45 +0800 Subject: [PATCH 039/179] [fix]: cursorConnt.color notify the text_line to repaint if it was disposed (#428) --- lib/src/widgets/text_line.dart | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index f87645ab..736a9c01 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -413,7 +413,7 @@ class RenderEditableTextLine extends RenderEditableBox { color = c; if (containsTextSelection()) { - markNeedsPaint(); + safeMarkNeedsPaint(); } } @@ -425,7 +425,7 @@ class RenderEditableTextLine extends RenderEditableBox { final containsSelection = containsTextSelection(); if (attached && containsCursor()) { cursorCont.removeListener(markNeedsLayout); - cursorCont.color.removeListener(markNeedsPaint); + cursorCont.color.removeListener(safeMarkNeedsPaint); } textSelection = t; @@ -433,11 +433,11 @@ class RenderEditableTextLine extends RenderEditableBox { _containsCursor = null; if (attached && containsCursor()) { cursorCont.addListener(markNeedsLayout); - cursorCont.color.addListener(markNeedsPaint); + cursorCont.color.addListener(safeMarkNeedsPaint); } if (containsSelection || containsTextSelection()) { - markNeedsPaint(); + safeMarkNeedsPaint(); } } @@ -642,7 +642,7 @@ class RenderEditableTextLine extends RenderEditableBox { } if (containsCursor()) { cursorCont.addListener(markNeedsLayout); - cursorCont.color.addListener(markNeedsPaint); + cursorCont.color.addListener(safeMarkNeedsPaint); } } @@ -654,7 +654,7 @@ class RenderEditableTextLine extends RenderEditableBox { } if (containsCursor()) { cursorCont.removeListener(markNeedsLayout); - cursorCont.color.removeListener(markNeedsPaint); + cursorCont.color.removeListener(safeMarkNeedsPaint); } } @@ -883,6 +883,14 @@ class RenderEditableTextLine extends RenderEditableBox { affinity: position.affinity, ); } + + void safeMarkNeedsPaint() { + if (!attached) { + //Should not paint if it was unattach. + return; + } + safeMarkNeedsPaint(); + } } class _TextLineElement extends RenderObjectElement { From c7158b55078cb402b372309ccedd33f33a40dcab Mon Sep 17 00:00:00 2001 From: Cheryl Date: Sat, 23 Oct 2021 14:26:06 -0700 Subject: [PATCH 040/179] Upgrade to 2.0.10 --- CHANGELOG.md | 3 +++ lib/src/widgets/text_line.dart | 4 ++-- pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c361b8c..2c5d1f13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.10] +* cursorConnt.color notify the text_line to repaint if it was disposed. + ## [2.0.9] * Improve UX when trying to add a link. diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index 736a9c01..ac81f217 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -886,10 +886,10 @@ class RenderEditableTextLine extends RenderEditableBox { void safeMarkNeedsPaint() { if (!attached) { - //Should not paint if it was unattach. + //Should not paint if it was unattached. return; } - safeMarkNeedsPaint(); + markNeedsPaint(); } } diff --git a/pubspec.yaml b/pubspec.yaml index 355b0239..6a0b5cb4 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: 2.0.9 +version: 2.0.10 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 4f0fa755b15130abfda1956a37f7505e4cfe2092 Mon Sep 17 00:00:00 2001 From: li3317 Date: Sat, 23 Oct 2021 22:18:14 -0400 Subject: [PATCH 041/179] Fix visibility of text selection handlers on scroll --- lib/src/widgets/editor.dart | 50 +++ .../quill_single_child_scroll_view.dart | 369 ++++++++++++++++++ lib/src/widgets/raw_editor.dart | 25 +- lib/src/widgets/simple_viewer.dart | 3 + 4 files changed, 445 insertions(+), 2 deletions(-) create mode 100644 lib/src/widgets/quill_single_child_scroll_view.dart diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 8a1e2b79..48524ca1 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -675,6 +675,7 @@ typedef TextSelectionChangedHandler = void Function( class RenderEditor extends RenderEditableContainerBox implements RenderAbstractEditor { RenderEditor( + ViewportOffset? offset, List? children, TextDirection textDirection, double scrollBottomInset, @@ -709,6 +710,41 @@ class RenderEditor extends RenderEditableContainerBox ValueListenable get selectionEndInViewport => _selectionEndInViewport; final ValueNotifier _selectionEndInViewport = ValueNotifier(true); + void _updateSelectionExtentsVisibility(Offset effectiveOffset) { + final visibleRegion = Offset.zero & size; + final startPosition = + TextPosition(offset: selection.start, affinity: selection.affinity); + final startOffset = _getOffsetForCaret(startPosition); + // TODO(justinmc): https://github.com/flutter/flutter/issues/31495 + // Check if the selection is visible with an approximation because a + // difference between rounded and unrounded values causes the caret to be + // reported as having a slightly (< 0.5) negative y offset. This rounding + // happens in paragraph.cc's layout and TextPainer's + // _applyFloatingPointHack. Ideally, the rounding mismatch will be fixed and + // this can be changed to be a strict check instead of an approximation. + const visibleRegionSlop = 0.5; + _selectionStartInViewport.value = visibleRegion + .inflate(visibleRegionSlop) + .contains(startOffset + effectiveOffset); + + final endPosition = + TextPosition(offset: selection.end, affinity: selection.affinity); + final endOffset = _getOffsetForCaret(endPosition); + _selectionEndInViewport.value = visibleRegion + .inflate(visibleRegionSlop) + .contains(endOffset + effectiveOffset); + } + + // returns offset relative to this at which the caret will be painted + // given a global TextPosition + Offset _getOffsetForCaret(TextPosition position) { + final child = childAtPosition(position); + final childPosition = child.globalToLocalPosition(position); + final boxParentData = child.parentData as BoxParentData; + final localOffsetForCaret = child.getOffsetForCaret(childPosition); + return boxParentData.offset + localOffsetForCaret; + } + void setDocument(Document doc) { if (document == doc) { return; @@ -725,6 +761,19 @@ class RenderEditor extends RenderEditableContainerBox markNeedsSemanticsUpdate(); } + Offset get _paintOffset => Offset(0, -(offset?.pixels ?? 0.0)); + + ViewportOffset? get offset => _offset; + ViewportOffset? _offset; + + set offset(ViewportOffset? value) { + if (_offset == value) return; + if (attached) _offset?.removeListener(markNeedsPaint); + _offset = value; + if (attached) _offset?.addListener(markNeedsPaint); + markNeedsLayout(); + } + void setSelection(TextSelection t) { if (selection == t) { return; @@ -958,6 +1007,7 @@ class RenderEditor extends RenderEditableContainerBox @override void paint(PaintingContext context, Offset offset) { defaultPaint(context, offset); + _updateSelectionExtentsVisibility(offset + _paintOffset); _paintHandleLayers(context, getEndpointsForSelection(selection)); } diff --git a/lib/src/widgets/quill_single_child_scroll_view.dart b/lib/src/widgets/quill_single_child_scroll_view.dart new file mode 100644 index 00000000..df5cd220 --- /dev/null +++ b/lib/src/widgets/quill_single_child_scroll_view.dart @@ -0,0 +1,369 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +/// Very similar to [SingleChildView] but with a [ViewportBuilder] argument +/// instead of a [Widget] +/// +/// Useful when child needs [ViewportOffset] (e.g. [RenderEditor]) +/// see: [SingleChildScrollView] +class QuillSingleChildScrollView extends StatelessWidget { + /// Creates a box in which a single widget can be scrolled. + const QuillSingleChildScrollView({ + required this.controller, + required this.viewportBuilder, + Key? key, + this.physics, + this.restorationId, + }) : super(key: key); + + /// An object that can be used to control the position to which this scroll + /// view is scrolled. + /// + /// Must be null if [primary] is true. + /// + /// A [ScrollController] serves several purposes. It can be used to control + /// the initial scroll position (see [ScrollController.initialScrollOffset]). + /// It can be used to control whether the scroll view should automatically + /// save and restore its scroll position in the [PageStorage] (see + /// [ScrollController.keepScrollOffset]). It can be used to read the current + /// scroll position (see [ScrollController.offset]), or change it (see + /// [ScrollController.animateTo]). + final ScrollController controller; + + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. + final ScrollPhysics? physics; + + /// {@macro flutter.widgets.scrollable.restorationId} + final String? restorationId; + + final ViewportBuilder viewportBuilder; + + AxisDirection _getDirection(BuildContext context) { + return getAxisDirectionFromAxisReverseAndDirectionality( + context, Axis.vertical, false); + } + + @override + Widget build(BuildContext context) { + final axisDirection = _getDirection(context); + final scrollController = controller; + final scrollable = Scrollable( + axisDirection: axisDirection, + controller: scrollController, + physics: physics, + restorationId: restorationId, + viewportBuilder: (context, offset) { + return _SingleChildViewport( + offset: offset, + child: viewportBuilder(context, offset), + ); + }, + ); + return scrollable; + } +} + +class _SingleChildViewport extends SingleChildRenderObjectWidget { + const _SingleChildViewport({ + required this.offset, + Key? key, + Widget? child, + }) : super(key: key, child: child); + + final ViewportOffset offset; + + @override + _RenderSingleChildViewport createRenderObject(BuildContext context) { + return _RenderSingleChildViewport( + offset: offset, + ); + } + + @override + void updateRenderObject( + BuildContext context, _RenderSingleChildViewport renderObject) { + // Order dependency: The offset setter reads the axis direction. + renderObject.offset = offset; + } +} + +class _RenderSingleChildViewport extends RenderBox + with RenderObjectWithChildMixin + implements RenderAbstractViewport { + _RenderSingleChildViewport({ + required ViewportOffset offset, + double cacheExtent = RenderAbstractViewport.defaultCacheExtent, + RenderBox? child, + }) : _offset = offset, + _cacheExtent = cacheExtent { + this.child = child; + } + + ViewportOffset get offset => _offset; + ViewportOffset _offset; + + set offset(ViewportOffset value) { + if (value == _offset) return; + if (attached) _offset.removeListener(_hasScrolled); + _offset = value; + if (attached) _offset.addListener(_hasScrolled); + markNeedsLayout(); + } + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + double get cacheExtent => _cacheExtent; + double _cacheExtent; + + set cacheExtent(double value) { + if (value == _cacheExtent) return; + _cacheExtent = value; + markNeedsLayout(); + } + + void _hasScrolled() { + markNeedsPaint(); + markNeedsSemanticsUpdate(); + } + + @override + void setupParentData(RenderObject child) { + // We don't actually use the offset argument in BoxParentData, so let's + // avoid allocating it at all. + if (child.parentData is! ParentData) child.parentData = ParentData(); + } + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + _offset.addListener(_hasScrolled); + } + + @override + void detach() { + _offset.removeListener(_hasScrolled); + super.detach(); + } + + @override + bool get isRepaintBoundary => true; + + double get _viewportExtent { + assert(hasSize); + return size.height; + } + + double get _minScrollExtent { + assert(hasSize); + return 0; + } + + double get _maxScrollExtent { + assert(hasSize); + if (child == null) return 0; + return math.max(0, child!.size.height - size.height); + } + + BoxConstraints _getInnerConstraints(BoxConstraints constraints) { + return constraints.widthConstraints(); + } + + @override + double computeMinIntrinsicWidth(double height) { + if (child != null) return child!.getMinIntrinsicWidth(height); + return 0; + } + + @override + double computeMaxIntrinsicWidth(double height) { + if (child != null) return child!.getMaxIntrinsicWidth(height); + return 0; + } + + @override + double computeMinIntrinsicHeight(double width) { + if (child != null) return child!.getMinIntrinsicHeight(width); + return 0; + } + + @override + double computeMaxIntrinsicHeight(double width) { + if (child != null) return child!.getMaxIntrinsicHeight(width); + return 0; + } + + // We don't override computeDistanceToActualBaseline(), because we + // want the default behavior (returning null). Otherwise, as you + // scroll, it would shift in its parent if the parent was baseline-aligned, + // which makes no sense. + + @override + Size computeDryLayout(BoxConstraints constraints) { + if (child == null) { + return constraints.smallest; + } + final childSize = child!.getDryLayout(_getInnerConstraints(constraints)); + return constraints.constrain(childSize); + } + + @override + void performLayout() { + final constraints = this.constraints; + if (child == null) { + size = constraints.smallest; + } else { + child!.layout(_getInnerConstraints(constraints), parentUsesSize: true); + size = constraints.constrain(child!.size); + } + + offset.applyViewportDimension(_viewportExtent); + offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent); + } + + Offset get _paintOffset => _paintOffsetForPosition(offset.pixels); + + Offset _paintOffsetForPosition(double position) { + return Offset(0, -position); + } + + bool _shouldClipAtPaintOffset(Offset paintOffset) { + assert(child != null); + return paintOffset.dx < 0 || + paintOffset.dy < 0 || + paintOffset.dx + child!.size.width > size.width || + paintOffset.dy + child!.size.height > size.height; + } + + @override + void paint(PaintingContext context, Offset offset) { + if (child != null) { + final paintOffset = _paintOffset; + + void paintContents(PaintingContext context, Offset offset) { + context.paintChild(child!, offset + paintOffset); + } + + if (_shouldClipAtPaintOffset(paintOffset)) { + _clipRectLayer.layer = context.pushClipRect( + needsCompositing, + offset, + Offset.zero & size, + paintContents, + oldLayer: _clipRectLayer.layer, + ); + } else { + _clipRectLayer.layer = null; + paintContents(context, offset); + } + } + } + + final _clipRectLayer = LayerHandle(); + + @override + void applyPaintTransform(RenderBox child, Matrix4 transform) { + final paintOffset = _paintOffset; + transform.translate(paintOffset.dx, paintOffset.dy); + } + + @override + Rect? describeApproximatePaintClip(RenderObject? child) { + if (child != null && _shouldClipAtPaintOffset(_paintOffset)) { + return Offset.zero & size; + } + return null; + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + if (child != null) { + return result.addWithPaintOffset( + offset: _paintOffset, + position: position, + hitTest: (result, transformed) { + assert(transformed == position + -_paintOffset); + return child!.hitTest(result, position: transformed); + }, + ); + } + return false; + } + + @override + RevealedOffset getOffsetToReveal(RenderObject target, double alignment, + {Rect? rect}) { + rect ??= target.paintBounds; + if (target is! RenderBox) { + return RevealedOffset(offset: offset.pixels, rect: rect); + } + + final targetBox = target; + final transform = targetBox.getTransformTo(child); + final bounds = MatrixUtils.transformRect(transform, rect); + + final double leadingScrollOffset; + final double targetMainAxisExtent; + final double mainAxisExtent; + + mainAxisExtent = size.height; + leadingScrollOffset = bounds.top; + targetMainAxisExtent = bounds.height; + + final targetOffset = leadingScrollOffset - + (mainAxisExtent - targetMainAxisExtent) * alignment; + final targetRect = bounds.shift(_paintOffsetForPosition(targetOffset)); + return RevealedOffset(offset: targetOffset, rect: targetRect); + } + + @override + void showOnScreen({ + RenderObject? descendant, + Rect? rect, + Duration duration = Duration.zero, + Curve curve = Curves.ease, + }) { + if (!offset.allowImplicitScrolling) { + return super.showOnScreen( + descendant: descendant, + rect: rect, + duration: duration, + curve: curve, + ); + } + + final newRect = RenderViewportBase.showInViewport( + descendant: descendant, + viewport: this, + offset: offset, + rect: rect, + duration: duration, + curve: curve, + ); + super.showOnScreen( + rect: newRect, + duration: duration, + curve: curve, + ); + } + + @override + Rect describeSemanticsClip(RenderObject child) { + return Rect.fromLTRB( + semanticBounds.left, + semanticBounds.top - cacheExtent, + semanticBounds.right, + semanticBounds.bottom + cacheExtent, + ); + } +} diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 899ba5c3..e9502678 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -23,6 +23,7 @@ import 'delegate.dart'; import 'editor.dart'; import 'keyboard_listener.dart'; import 'proxy.dart'; +import 'quill_single_child_scroll_view.dart'; import 'raw_editor/raw_editor_state_keyboard_mixin.dart'; import 'raw_editor/raw_editor_state_selection_delegate_mixin.dart'; import 'raw_editor/raw_editor_state_text_input_client_mixin.dart'; @@ -174,10 +175,26 @@ class RawEditorState extends EditorState child = BaselineProxy( textStyle: _styles!.paragraph!.style, padding: baselinePadding, - child: SingleChildScrollView( + child: QuillSingleChildScrollView( controller: _scrollController, physics: widget.scrollPhysics, - child: child, + viewportBuilder: (_, offset) => CompositedTransformTarget( + link: _toolbarLayerLink, + child: _Editor( + key: _editorKey, + offset: offset, + document: widget.controller.document, + selection: widget.controller.selection, + hasFocus: _hasFocus, + textDirection: _textDirection, + startHandleLayerLink: _startHandleLayerLink, + endHandleLayerLink: _endHandleLayerLink, + onSelectionChanged: _handleSelectionChanged, + scrollBottomInset: widget.scrollBottomInset, + padding: widget.padding, + children: _buildChildren(_doc, context), + ), + ), ), ); } @@ -717,8 +734,10 @@ class _Editor extends MultiChildRenderObjectWidget { required this.onSelectionChanged, required this.scrollBottomInset, this.padding = EdgeInsets.zero, + this.offset, }) : super(key: key, children: children); + final ViewportOffset? offset; final Document document; final TextDirection textDirection; final bool hasFocus; @@ -732,6 +751,7 @@ class _Editor extends MultiChildRenderObjectWidget { @override RenderEditor createRenderObject(BuildContext context) { return RenderEditor( + offset, null, textDirection, scrollBottomInset, @@ -750,6 +770,7 @@ class _Editor extends MultiChildRenderObjectWidget { void updateRenderObject( BuildContext context, covariant RenderEditor renderObject) { renderObject + ..offset = offset ..document = document ..setContainer(document.root) ..textDirection = textDirection diff --git a/lib/src/widgets/simple_viewer.dart b/lib/src/widgets/simple_viewer.dart index 8d59686a..dc68fe2a 100644 --- a/lib/src/widgets/simple_viewer.dart +++ b/lib/src/widgets/simple_viewer.dart @@ -316,10 +316,12 @@ class _SimpleViewer extends MultiChildRenderObjectWidget { required this.endHandleLayerLink, required this.onSelectionChanged, required this.scrollBottomInset, + this.offset, this.padding = EdgeInsets.zero, Key? key, }) : super(key: key, children: children); + final ViewportOffset? offset; final Document document; final TextDirection textDirection; final LayerLink startHandleLayerLink; @@ -331,6 +333,7 @@ class _SimpleViewer extends MultiChildRenderObjectWidget { @override RenderEditor createRenderObject(BuildContext context) { return RenderEditor( + offset, null, textDirection, scrollBottomInset, From b029812c9ae2dfdf6f34166a58ef31e294952744 Mon Sep 17 00:00:00 2001 From: li3317 Date: Sat, 23 Oct 2021 22:20:11 -0400 Subject: [PATCH 042/179] upgrade to 2.0.11 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c5d1f13..75bf71c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.11] +* Fix visibility of text selection handlers on scroll. + ## [2.0.10] * cursorConnt.color notify the text_line to repaint if it was disposed. diff --git a/pubspec.yaml b/pubspec.yaml index 6a0b5cb4..b40178c9 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: 2.0.10 +version: 2.0.11 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From c71747d4ef59449b14648b2c6ac9679c3da9da0d Mon Sep 17 00:00:00 2001 From: Cheryl Date: Sat, 23 Oct 2021 19:33:23 -0700 Subject: [PATCH 043/179] Remove comment --- lib/src/widgets/quill_single_child_scroll_view.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/src/widgets/quill_single_child_scroll_view.dart b/lib/src/widgets/quill_single_child_scroll_view.dart index df5cd220..a226f2b8 100644 --- a/lib/src/widgets/quill_single_child_scroll_view.dart +++ b/lib/src/widgets/quill_single_child_scroll_view.dart @@ -1,7 +1,3 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - import 'dart:math' as math; import 'package:flutter/material.dart'; From 926afac5f50f693fe15cef2b46cea5c7d0ba1280 Mon Sep 17 00:00:00 2001 From: Cheryl Date: Sat, 23 Oct 2021 19:36:06 -0700 Subject: [PATCH 044/179] Fix style issue --- lib/src/widgets/quill_single_child_scroll_view.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/widgets/quill_single_child_scroll_view.dart b/lib/src/widgets/quill_single_child_scroll_view.dart index a226f2b8..77b127b1 100644 --- a/lib/src/widgets/quill_single_child_scroll_view.dart +++ b/lib/src/widgets/quill_single_child_scroll_view.dart @@ -223,8 +223,9 @@ class _RenderSingleChildViewport extends RenderBox size = constraints.constrain(child!.size); } - offset.applyViewportDimension(_viewportExtent); - offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent); + offset + ..applyViewportDimension(_viewportExtent) + ..applyContentDimensions(_minScrollExtent, _maxScrollExtent); } Offset get _paintOffset => _paintOffsetForPosition(offset.pixels); From 7bf237f3c3c120008c333633a05e3172336c9bb8 Mon Sep 17 00:00:00 2001 From: appflowy <86001920+appflowy@users.noreply.github.com> Date: Mon, 25 Oct 2021 13:26:35 +0800 Subject: [PATCH 045/179] [fix]: The selection effect can't be seen as the textLine with background color (#429) --- lib/src/widgets/text_line.dart | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index ac81f217..b295de24 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -812,15 +812,6 @@ class RenderEditableTextLine extends RenderEditableBox { if (_body != null) { final parentData = _body!.parentData as BoxParentData; final effectiveOffset = offset + parentData.offset; - if (enableInteractiveSelection && - line.documentOffset <= textSelection.end && - textSelection.start <= line.documentOffset + line.length - 1) { - final local = localSelection(line, textSelection, false); - _selectedRects ??= _body!.getBoxesForSelection( - local, - ); - _paintSelection(context, effectiveOffset); - } if (hasFocus && cursorCont.show.value && @@ -837,6 +828,17 @@ class RenderEditableTextLine extends RenderEditableBox { cursorCont.style.paintAboveText) { _paintCursor(context, effectiveOffset, line.hasEmbed); } + + // paint the selection on the top + if (enableInteractiveSelection && + line.documentOffset <= textSelection.end && + textSelection.start <= line.documentOffset + line.length - 1) { + final local = localSelection(line, textSelection, false); + _selectedRects ??= _body!.getBoxesForSelection( + local, + ); + _paintSelection(context, effectiveOffset); + } } } From da8e1eec3ae4132981bf58d130e92abdb8a16b5d Mon Sep 17 00:00:00 2001 From: Cheryl Date: Mon, 25 Oct 2021 06:45:07 -0700 Subject: [PATCH 046/179] Upgrade to 2.0.12 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75bf71c2..de45d38e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.12] +* Fix the selection effect can't be seen as the textLine with background color. + ## [2.0.11] * Fix visibility of text selection handlers on scroll. diff --git a/pubspec.yaml b/pubspec.yaml index b40178c9..86a17eb3 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: 2.0.11 +version: 2.0.12 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 2fd395a21a3b82ecfe558c82accde8a5f51d69ea Mon Sep 17 00:00:00 2001 From: appflowy <86001920+appflowy@users.noreply.github.com> Date: Tue, 26 Oct 2021 13:20:54 +0800 Subject: [PATCH 047/179] Improve the scrolling performance by reducing the repaint areas (#430) --- lib/src/widgets/text_line.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index b295de24..c4b6cc17 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -579,6 +579,9 @@ class RenderEditableTextLine extends RenderEditableBox { return _getPosition(position, 1.5); } + @override + bool get isRepaintBoundary => true; + TextPosition? _getPosition(TextPosition textPosition, double dyScale) { assert(textPosition.offset < line.length); final offset = getOffsetForCaret(textPosition) From 2f4606decd2a5df2d8d474deef35eb56f9ac560f Mon Sep 17 00:00:00 2001 From: li3317 Date: Tue, 26 Oct 2021 09:13:28 -0400 Subject: [PATCH 048/179] upgrade to 2.0.13 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de45d38e..b8bb24d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.13] +Improve the scrolling performance by reducing the repaint areas. + ## [2.0.12] * Fix the selection effect can't be seen as the textLine with background color. diff --git a/pubspec.yaml b/pubspec.yaml index 86a17eb3..bb02d973 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: 2.0.12 +version: 2.0.13 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 0270611cd2442a3fa5f34143bc343f6fc2a256f4 Mon Sep 17 00:00:00 2001 From: Cheryl Date: Tue, 26 Oct 2021 09:24:36 -0700 Subject: [PATCH 049/179] Fix change log --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8bb24d9..be5c07b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## [2.0.13] -Improve the scrolling performance by reducing the repaint areas. +* Improve the scrolling performance by reducing the repaint areas. ## [2.0.12] * Fix the selection effect can't be seen as the textLine with background color. From 10d35392bd6fa7dce8970c18eb9843efd24d7045 Mon Sep 17 00:00:00 2001 From: Abdulmohsen Alkhamees Date: Thu, 28 Oct 2021 18:58:20 +0300 Subject: [PATCH 050/179] Arabic Locale (#433) --- lib/src/translations/toolbar.i18n.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index a20abf36..4782c154 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -12,6 +12,15 @@ extension Localization on String { 'Please first select some text to transform into a link.': 'Please first select some text to transform into a link.', }, + 'ar': { + 'Paste a link': 'نسخ الرابط', + 'Ok': 'نعم', + 'Select Color': 'اختار اللون', + 'Gallery': 'الصور', + 'Link': 'الرابط', + 'Please first select some text to transform into a link.': + 'يرجى اختيار نص للتحويل إلى رابط', + }, 'de': { 'Paste a link': 'Link hinzufügen', 'Ok': 'Ok', From 71a8f757663340927a3e6b630e2b8f5da338462d Mon Sep 17 00:00:00 2001 From: appflowy <86001920+appflowy@users.noreply.github.com> Date: Sun, 31 Oct 2021 21:43:15 +0800 Subject: [PATCH 051/179] Extract the block style widgets from text_block.dart to style_widgets folder (#435) --- .../widgets/style_widgets/bullet_point.dart | 22 +++ lib/src/widgets/style_widgets/checkbox.dart | 39 ++++ .../widgets/style_widgets/number_point.dart | 108 +++++++++++ .../widgets/style_widgets/style_widgets.dart | 3 + lib/src/widgets/text_block.dart | 174 +----------------- 5 files changed, 178 insertions(+), 168 deletions(-) create mode 100644 lib/src/widgets/style_widgets/bullet_point.dart create mode 100644 lib/src/widgets/style_widgets/checkbox.dart create mode 100644 lib/src/widgets/style_widgets/number_point.dart create mode 100644 lib/src/widgets/style_widgets/style_widgets.dart diff --git a/lib/src/widgets/style_widgets/bullet_point.dart b/lib/src/widgets/style_widgets/bullet_point.dart new file mode 100644 index 00000000..ee33c93a --- /dev/null +++ b/lib/src/widgets/style_widgets/bullet_point.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class QuillBulletPoint extends StatelessWidget { + const QuillBulletPoint({ + required this.style, + required this.width, + Key? key, + }) : super(key: key); + + final TextStyle style; + final double width; + + @override + Widget build(BuildContext context) { + return Container( + alignment: AlignmentDirectional.topEnd, + width: width, + padding: const EdgeInsetsDirectional.only(end: 13), + child: Text('•', style: style), + ); + } +} diff --git a/lib/src/widgets/style_widgets/checkbox.dart b/lib/src/widgets/style_widgets/checkbox.dart new file mode 100644 index 00000000..ee2eb816 --- /dev/null +++ b/lib/src/widgets/style_widgets/checkbox.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +class QuillCheckbox extends StatelessWidget { + const QuillCheckbox({ + Key? key, + this.style, + this.width, + this.isChecked = false, + this.offset, + this.onTap, + }) : super(key: key); + final TextStyle? style; + final double? width; + final bool isChecked; + final int? offset; + final Function(int, bool)? onTap; + + void _onCheckboxClicked(bool? newValue) { + if (onTap != null && newValue != null && offset != null) { + onTap!(offset!, newValue); + } + } + + @override + Widget build(BuildContext context) { + return Container( + alignment: AlignmentDirectional.topEnd, + width: width, + padding: const EdgeInsetsDirectional.only(end: 13), + child: GestureDetector( + onLongPress: () => _onCheckboxClicked(!isChecked), + child: Checkbox( + value: isChecked, + onChanged: _onCheckboxClicked, + ), + ), + ); + } +} diff --git a/lib/src/widgets/style_widgets/number_point.dart b/lib/src/widgets/style_widgets/number_point.dart new file mode 100644 index 00000000..6debf548 --- /dev/null +++ b/lib/src/widgets/style_widgets/number_point.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import '/models/documents/attribute.dart'; +import '/widgets/text_block.dart'; + + +class QuillNumberPoint extends StatelessWidget { + const QuillNumberPoint({ + required this.index, + required this.indentLevelCounts, + required this.count, + required this.style, + required this.width, + required this.attrs, + this.withDot = true, + this.padding = 0.0, + Key? key, + }) : super(key: key); + + final int index; + final Map indentLevelCounts; + final int count; + final TextStyle style; + final double width; + final Map attrs; + final bool withDot; + final double padding; + + @override + Widget build(BuildContext context) { + var s = index.toString(); + int? level = 0; + if (!attrs.containsKey(Attribute.indent.key) && + !indentLevelCounts.containsKey(1)) { + indentLevelCounts.clear(); + return Container( + alignment: AlignmentDirectional.topEnd, + width: width, + padding: EdgeInsetsDirectional.only(end: padding), + child: Text(withDot ? '$s.' : s, style: style), + ); + } + if (attrs.containsKey(Attribute.indent.key)) { + level = attrs[Attribute.indent.key]!.value; + } else { + // first level but is back from previous indent level + // supposed to be "2." + indentLevelCounts[0] = 1; + } + if (indentLevelCounts.containsKey(level! + 1)) { + // last visited level is done, going up + indentLevelCounts.remove(level + 1); + } + final count = (indentLevelCounts[level] ?? 0) + 1; + indentLevelCounts[level] = count; + + s = count.toString(); + if (level % 3 == 1) { + // a. b. c. d. e. ... + s = _toExcelSheetColumnTitle(count); + } else if (level % 3 == 2) { + // i. ii. iii. ... + s = _intToRoman(count); + } + // level % 3 == 0 goes back to 1. 2. 3. + + return Container( + alignment: AlignmentDirectional.topEnd, + width: width, + padding: EdgeInsetsDirectional.only(end: padding), + child: Text(withDot ? '$s.' : s, style: style), + ); + } + + String _toExcelSheetColumnTitle(int n) { + final result = StringBuffer(); + while (n > 0) { + n--; + result.write(String.fromCharCode((n % 26).floor() + 97)); + n = (n / 26).floor(); + } + + return result.toString().split('').reversed.join(); + } + + String _intToRoman(int input) { + var num = input; + + if (num < 0) { + return ''; + } else if (num == 0) { + return 'nulla'; + } + + final builder = StringBuffer(); + for (var a = 0; a < arabianRomanNumbers.length; a++) { + final times = (num / arabianRomanNumbers[a]) + .truncate(); // equals 1 only when arabianRomanNumbers[a] = num + // executes n times where n is the number of times you have to add + // the current roman number value to reach current num. + builder.write(romanNumbers[a] * times); + num -= times * + arabianRomanNumbers[ + a]; // subtract previous roman number value from num + } + + return builder.toString().toLowerCase(); + } +} diff --git a/lib/src/widgets/style_widgets/style_widgets.dart b/lib/src/widgets/style_widgets/style_widgets.dart new file mode 100644 index 00000000..0f0dd0a5 --- /dev/null +++ b/lib/src/widgets/style_widgets/style_widgets.dart @@ -0,0 +1,3 @@ +export 'bullet_point.dart'; +export 'checkbox.dart'; +export 'number_point.dart'; \ No newline at end of file diff --git a/lib/src/widgets/text_block.dart b/lib/src/widgets/text_block.dart index e46cba0d..132ff145 100644 --- a/lib/src/widgets/text_block.dart +++ b/lib/src/widgets/text_block.dart @@ -6,6 +6,7 @@ import 'package:tuple/tuple.dart'; import '../models/documents/attribute.dart'; import '../models/documents/nodes/block.dart'; import '../models/documents/nodes/line.dart'; +import '../widgets/style_widgets/style_widgets.dart'; import 'box.dart'; import 'cursor.dart'; import 'default_styles.dart'; @@ -148,7 +149,7 @@ class EditableTextBlock extends StatelessWidget { final defaultStyles = QuillStyles.getStyles(context, false); final attrs = line.style.attributes; if (attrs[Attribute.list.key] == Attribute.ol) { - return _NumberPoint( + return QuillNumberPoint( index: index, indentLevelCounts: indentLevelCounts, count: count, @@ -160,7 +161,7 @@ class EditableTextBlock extends StatelessWidget { } if (attrs[Attribute.list.key] == Attribute.ul) { - return _BulletPoint( + return QuillBulletPoint( style: defaultStyles!.leading!.style.copyWith(fontWeight: FontWeight.bold), width: 32, @@ -168,7 +169,7 @@ class EditableTextBlock extends StatelessWidget { } if (attrs[Attribute.list.key] == Attribute.checked) { - return _Checkbox( + return QuillCheckbox( key: UniqueKey(), style: defaultStyles!.leading!.style, width: 32, @@ -179,7 +180,7 @@ class EditableTextBlock extends StatelessWidget { } if (attrs[Attribute.list.key] == Attribute.unchecked) { - return _Checkbox( + return QuillCheckbox( key: UniqueKey(), style: defaultStyles!.leading!.style, width: 32, @@ -189,7 +190,7 @@ class EditableTextBlock extends StatelessWidget { } if (attrs.containsKey(Attribute.codeBlock.key)) { - return _NumberPoint( + return QuillNumberPoint( index: index, indentLevelCounts: indentLevelCounts, count: count, @@ -607,166 +608,3 @@ class _EditableBlock extends MultiChildRenderObjectWidget { ..contentPadding = _contentPadding; } } - -class _NumberPoint extends StatelessWidget { - const _NumberPoint({ - required this.index, - required this.indentLevelCounts, - required this.count, - required this.style, - required this.width, - required this.attrs, - this.withDot = true, - this.padding = 0.0, - Key? key, - }) : super(key: key); - - final int index; - final Map indentLevelCounts; - final int count; - final TextStyle style; - final double width; - final Map attrs; - final bool withDot; - final double padding; - - @override - Widget build(BuildContext context) { - var s = index.toString(); - int? level = 0; - if (!attrs.containsKey(Attribute.indent.key) && - !indentLevelCounts.containsKey(1)) { - indentLevelCounts.clear(); - return Container( - alignment: AlignmentDirectional.topEnd, - width: width, - padding: EdgeInsetsDirectional.only(end: padding), - child: Text(withDot ? '$s.' : s, style: style), - ); - } - if (attrs.containsKey(Attribute.indent.key)) { - level = attrs[Attribute.indent.key]!.value; - } else { - // first level but is back from previous indent level - // supposed to be "2." - indentLevelCounts[0] = 1; - } - if (indentLevelCounts.containsKey(level! + 1)) { - // last visited level is done, going up - indentLevelCounts.remove(level + 1); - } - final count = (indentLevelCounts[level] ?? 0) + 1; - indentLevelCounts[level] = count; - - s = count.toString(); - if (level % 3 == 1) { - // a. b. c. d. e. ... - s = _toExcelSheetColumnTitle(count); - } else if (level % 3 == 2) { - // i. ii. iii. ... - s = _intToRoman(count); - } - // level % 3 == 0 goes back to 1. 2. 3. - - return Container( - alignment: AlignmentDirectional.topEnd, - width: width, - padding: EdgeInsetsDirectional.only(end: padding), - child: Text(withDot ? '$s.' : s, style: style), - ); - } - - String _toExcelSheetColumnTitle(int n) { - final result = StringBuffer(); - while (n > 0) { - n--; - result.write(String.fromCharCode((n % 26).floor() + 97)); - n = (n / 26).floor(); - } - - return result.toString().split('').reversed.join(); - } - - String _intToRoman(int input) { - var num = input; - - if (num < 0) { - return ''; - } else if (num == 0) { - return 'nulla'; - } - - final builder = StringBuffer(); - for (var a = 0; a < arabianRomanNumbers.length; a++) { - final times = (num / arabianRomanNumbers[a]) - .truncate(); // equals 1 only when arabianRomanNumbers[a] = num - // executes n times where n is the number of times you have to add - // the current roman number value to reach current num. - builder.write(romanNumbers[a] * times); - num -= times * - arabianRomanNumbers[ - a]; // subtract previous roman number value from num - } - - return builder.toString().toLowerCase(); - } -} - -class _BulletPoint extends StatelessWidget { - const _BulletPoint({ - required this.style, - required this.width, - Key? key, - }) : super(key: key); - - final TextStyle style; - final double width; - - @override - Widget build(BuildContext context) { - return Container( - alignment: AlignmentDirectional.topEnd, - width: width, - padding: const EdgeInsetsDirectional.only(end: 13), - child: Text('•', style: style), - ); - } -} - -class _Checkbox extends StatelessWidget { - const _Checkbox({ - Key? key, - this.style, - this.width, - this.isChecked = false, - this.offset, - this.onTap, - }) : super(key: key); - final TextStyle? style; - final double? width; - final bool isChecked; - final int? offset; - final Function(int, bool)? onTap; - - void _onCheckboxClicked(bool? newValue) { - if (onTap != null && newValue != null && offset != null) { - onTap!(offset!, newValue); - } - } - - @override - Widget build(BuildContext context) { - return Container( - alignment: AlignmentDirectional.topEnd, - width: width, - padding: const EdgeInsetsDirectional.only(end: 13), - child: GestureDetector( - onLongPress: () => _onCheckboxClicked(!isChecked), - child: Checkbox( - value: isChecked, - onChanged: _onCheckboxClicked, - ), - ), - ); - } -} From 230f091e4ce6ce99a9a616e6084e8e300ce774e4 Mon Sep 17 00:00:00 2001 From: appflowy <86001920+appflowy@users.noreply.github.com> Date: Mon, 1 Nov 2021 23:14:17 +0800 Subject: [PATCH 052/179] enable customize the checkbox widget using DefaultListBlockStyle style. (#436) --- lib/flutter_quill.dart | 1 + lib/src/widgets/default_styles.dart | 19 +++++++-- lib/src/widgets/style_widgets/checkbox.dart | 43 +++++++++++++++------ lib/src/widgets/text_block.dart | 2 + 4 files changed, 51 insertions(+), 14 deletions(-) diff --git a/lib/flutter_quill.dart b/lib/flutter_quill.dart index c82b164f..bc1343ee 100644 --- a/lib/flutter_quill.dart +++ b/lib/flutter_quill.dart @@ -10,4 +10,5 @@ export 'src/models/themes/quill_icon_theme.dart'; export 'src/widgets/controller.dart'; export 'src/widgets/default_styles.dart'; export 'src/widgets/editor.dart'; +export 'src/widgets/style_widgets/style_widgets.dart'; export 'src/widgets/toolbar.dart'; diff --git a/lib/src/widgets/default_styles.dart b/lib/src/widgets/default_styles.dart index ea583909..eb882d0a 100644 --- a/lib/src/widgets/default_styles.dart +++ b/lib/src/widgets/default_styles.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_quill/src/widgets/style_widgets/style_widgets.dart'; import 'package:tuple/tuple.dart'; class QuillStyles extends InheritedWidget { @@ -43,6 +44,18 @@ class DefaultTextBlockStyle { final BoxDecoration? decoration; } +class DefaultListBlockStyle extends DefaultTextBlockStyle { + DefaultListBlockStyle( + TextStyle style, + Tuple2 verticalSpacing, + Tuple2 lineSpacing, + BoxDecoration? decoration, + this.checkboxUIBuilder, + ) : super(style, verticalSpacing, lineSpacing, decoration); + + final QuillCheckboxBuilder? checkboxUIBuilder; +} + class DefaultStyles { DefaultStyles({ this.h1, @@ -85,7 +98,7 @@ class DefaultStyles { final TextStyle? link; final Color? color; final DefaultTextBlockStyle? placeHolder; - final DefaultTextBlockStyle? lists; + final DefaultListBlockStyle? lists; final DefaultTextBlockStyle? quote; final DefaultTextBlockStyle? code; final DefaultTextBlockStyle? indent; @@ -172,8 +185,8 @@ class DefaultStyles { const Tuple2(0, 0), const Tuple2(0, 0), null), - lists: DefaultTextBlockStyle( - baseStyle, baseSpacing, const Tuple2(0, 6), null), + lists: DefaultListBlockStyle( + baseStyle, baseSpacing, const Tuple2(0, 6), null, null), quote: DefaultTextBlockStyle( TextStyle(color: baseStyle.color!.withOpacity(0.6)), baseSpacing, diff --git a/lib/src/widgets/style_widgets/checkbox.dart b/lib/src/widgets/style_widgets/checkbox.dart index ee2eb816..64f36120 100644 --- a/lib/src/widgets/style_widgets/checkbox.dart +++ b/lib/src/widgets/style_widgets/checkbox.dart @@ -8,12 +8,14 @@ class QuillCheckbox extends StatelessWidget { this.isChecked = false, this.offset, this.onTap, + this.uiBuilder, }) : super(key: key); final TextStyle? style; final double? width; final bool isChecked; final int? offset; final Function(int, bool)? onTap; + final QuillCheckboxBuilder? uiBuilder; void _onCheckboxClicked(bool? newValue) { if (onTap != null && newValue != null && offset != null) { @@ -23,17 +25,36 @@ class QuillCheckbox extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - alignment: AlignmentDirectional.topEnd, - width: width, - padding: const EdgeInsetsDirectional.only(end: 13), - child: GestureDetector( - onLongPress: () => _onCheckboxClicked(!isChecked), - child: Checkbox( - value: isChecked, - onChanged: _onCheckboxClicked, + Widget child; + if (uiBuilder != null) { + child = uiBuilder!.build( + context: context, + isChecked: isChecked, + onChanged: _onCheckboxClicked, + ); + } else { + child = Container( + alignment: AlignmentDirectional.topEnd, + width: width, + padding: const EdgeInsetsDirectional.only(end: 13), + child: GestureDetector( + onLongPress: () => _onCheckboxClicked(!isChecked), + child: Checkbox( + value: isChecked, + onChanged: _onCheckboxClicked, + ), ), - ), - ); + ); + } + + return child; } } + +abstract class QuillCheckboxBuilder { + Widget build({ + required BuildContext context, + required bool isChecked, + required void Function(bool?) onChanged, + }); +} diff --git a/lib/src/widgets/text_block.dart b/lib/src/widgets/text_block.dart index 132ff145..40fc351b 100644 --- a/lib/src/widgets/text_block.dart +++ b/lib/src/widgets/text_block.dart @@ -176,6 +176,7 @@ class EditableTextBlock extends StatelessWidget { isChecked: true, offset: block.offset + line.offset, onTap: onCheckboxTap, + uiBuilder: defaultStyles.lists!.checkboxUIBuilder, ); } @@ -186,6 +187,7 @@ class EditableTextBlock extends StatelessWidget { width: 32, offset: block.offset + line.offset, onTap: onCheckboxTap, + uiBuilder: defaultStyles.lists!.checkboxUIBuilder, ); } From 963f6186a3f5d5c439256dd1024aa536bbe74639 Mon Sep 17 00:00:00 2001 From: li3317 Date: Mon, 1 Nov 2021 12:28:05 -0400 Subject: [PATCH 053/179] upgrade to 2.0.14 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be5c07b8..8175c9d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.14] +* enable customize the checkbox widget using DefaultListBlockStyle style. + ## [2.0.13] * Improve the scrolling performance by reducing the repaint areas. diff --git a/pubspec.yaml b/pubspec.yaml index bb02d973..ddd7eec8 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: 2.0.13 +version: 2.0.14 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 3a0add9a4c3ab09ebfadd4502d210a1092b6f9d1 Mon Sep 17 00:00:00 2001 From: Cheryl Date: Mon, 1 Nov 2021 16:02:15 -0700 Subject: [PATCH 054/179] Reformat code --- lib/src/widgets/default_styles.dart | 3 ++- lib/src/widgets/style_widgets/number_point.dart | 1 - lib/src/widgets/style_widgets/style_widgets.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/widgets/default_styles.dart b/lib/src/widgets/default_styles.dart index eb882d0a..79d5f55a 100644 --- a/lib/src/widgets/default_styles.dart +++ b/lib/src/widgets/default_styles.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_quill/src/widgets/style_widgets/style_widgets.dart'; import 'package:tuple/tuple.dart'; +import 'style_widgets/style_widgets.dart'; + class QuillStyles extends InheritedWidget { const QuillStyles({ required this.data, diff --git a/lib/src/widgets/style_widgets/number_point.dart b/lib/src/widgets/style_widgets/number_point.dart index 6debf548..16dd1e4f 100644 --- a/lib/src/widgets/style_widgets/number_point.dart +++ b/lib/src/widgets/style_widgets/number_point.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import '/models/documents/attribute.dart'; import '/widgets/text_block.dart'; - class QuillNumberPoint extends StatelessWidget { const QuillNumberPoint({ required this.index, diff --git a/lib/src/widgets/style_widgets/style_widgets.dart b/lib/src/widgets/style_widgets/style_widgets.dart index 0f0dd0a5..2a252d43 100644 --- a/lib/src/widgets/style_widgets/style_widgets.dart +++ b/lib/src/widgets/style_widgets/style_widgets.dart @@ -1,3 +1,3 @@ export 'bullet_point.dart'; export 'checkbox.dart'; -export 'number_point.dart'; \ No newline at end of file +export 'number_point.dart'; From 0b1e0f217981a9ba4613fdddba2326382e9ad8da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kh=C3=A1nh=20Nguy=E1=BB=85n?= Date: Tue, 2 Nov 2021 23:05:33 +0700 Subject: [PATCH 055/179] implement change cursor to SystemMouseCursors.click when hovering a link styled text (#437) Co-authored-by: KhanhNgocNguyen --- lib/src/widgets/text_line.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index c4b6cc17..e9e5f229 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -184,6 +184,7 @@ class TextLine extends StatelessWidget { final style = textNode.style; var res = const TextStyle(); // This is inline text style final color = textNode.style.attributes[Attribute.color.key]; + var hasLink = false; { Attribute.bold.key: defaultStyles.bold, @@ -203,6 +204,9 @@ class TextLine extends StatelessWidget { res = _merge(res.copyWith(decorationColor: textColor), s!.copyWith(decorationColor: textColor)); } else { + if (k == Attribute.link.key) { + hasLink = true; + } res = _merge(res, s!); } } @@ -252,6 +256,13 @@ class TextLine extends StatelessWidget { } res = _applyCustomAttributes(res, textNode.style.attributes); + if (hasLink && readOnly) { + return TextSpan( + text: textNode.value, + style: res, + mouseCursor: SystemMouseCursors.click, + ); + } return TextSpan(text: textNode.value, style: res); } From 013175f73721202d154644e6f70b2998bbf00eb1 Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 3 Nov 2021 10:04:03 -0700 Subject: [PATCH 056/179] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8386a922..bf4d87d0 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,7 @@ QuillToolbar(locale: Locale('fr'), ...) ``` Currently, translations are available for these locales: * `Locale('en')` +* `Locale('ar')` * `Locale('de')` * `Locale('fr')` * `Locale('zh', 'CN')` From 5b5ecf4b2488c8d9e12fb1a9b719906af747d048 Mon Sep 17 00:00:00 2001 From: li3317 Date: Sat, 6 Nov 2021 16:23:22 -0400 Subject: [PATCH 057/179] Upgrade to 2.0.15 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8175c9d3..965f80ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.15] +* implement change cursor to SystemMouseCursors.click when hovering a link styled text. + ## [2.0.14] * enable customize the checkbox widget using DefaultListBlockStyle style. diff --git a/pubspec.yaml b/pubspec.yaml index ddd7eec8..0f3377d7 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: 2.0.14 +version: 2.0.15 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 827a85a322c6421866e115c6411a37b99c10368a Mon Sep 17 00:00:00 2001 From: Develeste <93141030+Develeste@users.noreply.github.com> Date: Sun, 14 Nov 2021 15:19:32 +0900 Subject: [PATCH 058/179] Add Korean Locale Translations (#451) --- lib/src/translations/toolbar.i18n.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 4782c154..3d7a3d71 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -48,6 +48,15 @@ extension Localization on String { 'Link': '链接', 'Please first select some text to transform into a link.': '请先选择一些要转化为链接的文本', + }, + 'ko': { + 'Paste a link': '링크를 붙여넣어 주세요.', + 'Ok': '확인', + 'Select Color': '색상 선택', + 'Gallery': '갤러리', + 'Link': '링크', + 'Please first select some text to transform into a link.': + '링크로 전환할 글자를 먼저 선택해주세요.', } }; From 7f61a1713d629f102f63d87e82aa40ab0c7ad8d6 Mon Sep 17 00:00:00 2001 From: X Code Date: Sat, 13 Nov 2021 22:22:12 -0800 Subject: [PATCH 059/179] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bf4d87d0..787bef8f 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ Currently, translations are available for these locales: * `Locale('de')` * `Locale('fr')` * `Locale('zh', 'CN')` +* `Locale('ko')` ### Contributing to translations The translation file is located at [lib/src/translations/toolbar.i18n.dart](lib/src/translations/toolbar.i18n.dart). Feel free to contribute your own translations, just copy the English translations map and replace the values with your translations. Then open a pull request so everyone can benefit from your translations! From cfe5d78a8798e652b0afc0b48a323d1624084091 Mon Sep 17 00:00:00 2001 From: mark8044 <87546778+mark8044@users.noreply.github.com> Date: Sun, 14 Nov 2021 08:43:43 -0800 Subject: [PATCH 060/179] Update toolbar.dart (#458) --- lib/src/widgets/toolbar.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index a574d418..9d74ca45 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -281,6 +281,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { SelectAlignmentButton( controller: controller, iconSize: toolbarIconSize, + iconTheme: iconTheme, ), if (isButtonGroupShown[1] && (isButtonGroupShown[2] || From 733c9bd1b0ffa10e86d700a4301ddd0a797d0647 Mon Sep 17 00:00:00 2001 From: mark8044 <87546778+mark8044@users.noreply.github.com> Date: Sun, 14 Nov 2021 10:11:18 -0800 Subject: [PATCH 061/179] Add ability to hide/show Left, Center, Right and Justify alignment buttons (#460) --- lib/src/widgets/toolbar.dart | 12 +++++++ .../toolbar/select_alignment_button.dart | 36 ++++++++++++------- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 9d74ca45..1c9a0391 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -79,6 +79,10 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { bool showBackgroundColorButton = true, bool showClearFormat = true, bool showAlignmentButtons = false, + bool showLeftAlignment = true, + bool showCenterAlignment = true, + bool showRightAlignment = true, + bool showJustifyAlignment = true, bool showHeaderStyle = true, bool showListNumbers = true, bool showListBullets = true, @@ -130,6 +134,10 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { onImagePickCallback != null || onVideoPickCallback != null, showAlignmentButtons, + showLeftAlignment, + showCenterAlignment, + showRightAlignment, + showJustifyAlignment, showHeaderStyle, showListNumbers || showListBullets || showListCheck || showCodeBlock, showQuote || showIndent, @@ -282,6 +290,10 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { controller: controller, iconSize: toolbarIconSize, iconTheme: iconTheme, + showLeftAlignment: showLeftAlignment, + showCenterAlignment: showCenterAlignment, + showRightAlignment: showRightAlignment, + showJustifyAlignment: showJustifyAlignment, ), if (isButtonGroupShown[1] && (isButtonGroupShown[2] || diff --git a/lib/src/widgets/toolbar/select_alignment_button.dart b/lib/src/widgets/toolbar/select_alignment_button.dart index 9761a96d..df3abb8b 100644 --- a/lib/src/widgets/toolbar/select_alignment_button.dart +++ b/lib/src/widgets/toolbar/select_alignment_button.dart @@ -12,6 +12,10 @@ class SelectAlignmentButton extends StatefulWidget { required this.controller, this.iconSize = kDefaultIconSize, this.iconTheme, + this.showLeftAlignment, + this.showCenterAlignment, + this.showRightAlignment, + this.showJustifyAlignment, Key? key, }) : super(key: key); @@ -19,6 +23,10 @@ class SelectAlignmentButton extends StatefulWidget { final double iconSize; final QuillIconTheme? iconTheme; + final bool? showLeftAlignment; + final bool? showCenterAlignment; + final bool? showRightAlignment; + final bool? showJustifyAlignment; @override _SelectAlignmentButtonState createState() => _SelectAlignmentButtonState(); @@ -42,30 +50,32 @@ class _SelectAlignmentButtonState extends State { @override Widget build(BuildContext context) { final _valueToText = { - Attribute.leftAlignment: Attribute.leftAlignment.value!, - Attribute.centerAlignment: Attribute.centerAlignment.value!, - Attribute.rightAlignment: Attribute.rightAlignment.value!, - Attribute.justifyAlignment: Attribute.justifyAlignment.value!, + if (widget.showLeftAlignment!) Attribute.leftAlignment: Attribute.leftAlignment.value!, + if (widget.showCenterAlignment!) Attribute.centerAlignment: Attribute.centerAlignment.value!, + if (widget.showRightAlignment!) Attribute.rightAlignment: Attribute.rightAlignment.value!, + if (widget.showJustifyAlignment!) Attribute.justifyAlignment: Attribute.justifyAlignment.value!, }; final _valueAttribute = [ - Attribute.leftAlignment, - Attribute.centerAlignment, - Attribute.rightAlignment, - Attribute.justifyAlignment + if (widget.showLeftAlignment!) Attribute.leftAlignment, + if (widget.showCenterAlignment!) Attribute.centerAlignment, + if (widget.showRightAlignment!) Attribute.rightAlignment, + if (widget.showJustifyAlignment!) Attribute.justifyAlignment ]; final _valueString = [ - Attribute.leftAlignment.value!, - Attribute.centerAlignment.value!, - Attribute.rightAlignment.value!, - Attribute.justifyAlignment.value!, + if (widget.showLeftAlignment!) Attribute.leftAlignment.value!, + if (widget.showCenterAlignment!) Attribute.centerAlignment.value!, + if (widget.showRightAlignment!) Attribute.rightAlignment.value!, + if (widget.showJustifyAlignment!) Attribute.justifyAlignment.value!, ]; final theme = Theme.of(context); + + final buttonCount = ((widget.showLeftAlignment!) ? 1 : 0) + ((widget.showCenterAlignment!) ? 1 : 0) + ((widget.showRightAlignment!) ? 1 : 0) + ((widget.showJustifyAlignment!) ? 1 : 0); return Row( mainAxisSize: MainAxisSize.min, - children: List.generate(4, (index) { + children: List.generate(buttonCount, (index) { return Padding( padding: const EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0), child: ConstrainedBox( From 2db1cc931b8916da76388550af756a67f2995005 Mon Sep 17 00:00:00 2001 From: li3317 Date: Sun, 14 Nov 2021 20:48:43 -0500 Subject: [PATCH 062/179] format code --- lib/src/widgets/toolbar.dart | 8 ++++---- .../toolbar/select_alignment_button.dart | 19 +++++++++++++------ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 1c9a0391..92ce72a9 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -137,7 +137,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { showLeftAlignment, showCenterAlignment, showRightAlignment, - showJustifyAlignment, + showJustifyAlignment, showHeaderStyle, showListNumbers || showListBullets || showListCheck || showCodeBlock, showQuote || showIndent, @@ -290,9 +290,9 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { controller: controller, iconSize: toolbarIconSize, iconTheme: iconTheme, - showLeftAlignment: showLeftAlignment, - showCenterAlignment: showCenterAlignment, - showRightAlignment: showRightAlignment, + showLeftAlignment: showLeftAlignment, + showCenterAlignment: showCenterAlignment, + showRightAlignment: showRightAlignment, showJustifyAlignment: showJustifyAlignment, ), if (isButtonGroupShown[1] && diff --git a/lib/src/widgets/toolbar/select_alignment_button.dart b/lib/src/widgets/toolbar/select_alignment_button.dart index df3abb8b..98eb90ad 100644 --- a/lib/src/widgets/toolbar/select_alignment_button.dart +++ b/lib/src/widgets/toolbar/select_alignment_button.dart @@ -50,10 +50,14 @@ class _SelectAlignmentButtonState extends State { @override Widget build(BuildContext context) { final _valueToText = { - if (widget.showLeftAlignment!) Attribute.leftAlignment: Attribute.leftAlignment.value!, - if (widget.showCenterAlignment!) Attribute.centerAlignment: Attribute.centerAlignment.value!, - if (widget.showRightAlignment!) Attribute.rightAlignment: Attribute.rightAlignment.value!, - if (widget.showJustifyAlignment!) Attribute.justifyAlignment: Attribute.justifyAlignment.value!, + if (widget.showLeftAlignment!) + Attribute.leftAlignment: Attribute.leftAlignment.value!, + if (widget.showCenterAlignment!) + Attribute.centerAlignment: Attribute.centerAlignment.value!, + if (widget.showRightAlignment!) + Attribute.rightAlignment: Attribute.rightAlignment.value!, + if (widget.showJustifyAlignment!) + Attribute.justifyAlignment: Attribute.justifyAlignment.value!, }; final _valueAttribute = [ @@ -70,8 +74,11 @@ class _SelectAlignmentButtonState extends State { ]; final theme = Theme.of(context); - - final buttonCount = ((widget.showLeftAlignment!) ? 1 : 0) + ((widget.showCenterAlignment!) ? 1 : 0) + ((widget.showRightAlignment!) ? 1 : 0) + ((widget.showJustifyAlignment!) ? 1 : 0); + + final buttonCount = ((widget.showLeftAlignment!) ? 1 : 0) + + ((widget.showCenterAlignment!) ? 1 : 0) + + ((widget.showRightAlignment!) ? 1 : 0) + + ((widget.showJustifyAlignment!) ? 1 : 0); return Row( mainAxisSize: MainAxisSize.min, From 7e958b613ad380f716686036179c82c78f085226 Mon Sep 17 00:00:00 2001 From: li3317 Date: Sun, 14 Nov 2021 20:50:27 -0500 Subject: [PATCH 063/179] upgrade to 2.0.16 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 965f80ec..c61d60eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.16] +* Add hide / show alignment buttons. + ## [2.0.15] * implement change cursor to SystemMouseCursors.click when hovering a link styled text. diff --git a/pubspec.yaml b/pubspec.yaml index 0f3377d7..f21ef84c 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: 2.0.15 +version: 2.0.16 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 278980c6778ac8d658c4a48eae4207ef52e8b59d Mon Sep 17 00:00:00 2001 From: mark8044 <87546778+mark8044@users.noreply.github.com> Date: Sun, 14 Nov 2021 19:33:39 -0800 Subject: [PATCH 064/179] Add a new parameter toolBarSectionSpacing to control the toolbar icon Wrap spacing (#461) --- lib/src/widgets/toolbar.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 92ce72a9..947ed255 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -59,6 +59,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { const QuillToolbar({ required this.children, this.toolBarHeight = 36, + this.toolBarSectionSpacing, this.color, this.filePickImpl, this.multiRowsDisplay, @@ -69,6 +70,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { factory QuillToolbar.basic({ required QuillController controller, double toolbarIconSize = kDefaultIconSize, + double toolBarSectionSpacing = 4, bool showBoldButton = true, bool showItalicButton = true, bool showSmallButton = false, @@ -147,6 +149,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { return QuillToolbar( key: key, toolBarHeight: toolbarIconSize * 2, + toolBarSectionSpacing: toolBarSectionSpacing, multiRowsDisplay: multiRowsDisplay, locale: locale, children: [ @@ -409,6 +412,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { final List children; final double toolBarHeight; + final double? toolBarSectionSpacing; final bool? multiRowsDisplay; /// The color of the toolbar. @@ -438,7 +442,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { ? Wrap( alignment: WrapAlignment.center, runSpacing: 4, - spacing: 4, + spacing: toolBarSectionSpacing ?? 4, children: children, ) : Container( From 635867731f84a08857fa6198d8df0c4b24143e95 Mon Sep 17 00:00:00 2001 From: mark8044 <87546778+mark8044@users.noreply.github.com> Date: Sun, 14 Nov 2021 21:26:46 -0800 Subject: [PATCH 065/179] Allow alignment of the toolbar icons to match WrapAlignment (#462) --- lib/src/widgets/toolbar.dart | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 947ed255..80269eef 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -49,6 +49,8 @@ typedef WebVideoPickImpl = Future Function( typedef MediaPickSettingSelector = Future Function( BuildContext context); +enum ToolbarAlignment{ start, center, end } + // The default size of the icon of a button. const double kDefaultIconSize = 18; @@ -60,6 +62,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { required this.children, this.toolBarHeight = 36, this.toolBarSectionSpacing, + this.toolBarIconAlignment, this.color, this.filePickImpl, this.multiRowsDisplay, @@ -71,6 +74,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { required QuillController controller, double toolbarIconSize = kDefaultIconSize, double toolBarSectionSpacing = 4, + ToolbarAlignment toolBarIconAlignment = ToolbarAlignment.center, bool showBoldButton = true, bool showItalicButton = true, bool showSmallButton = false, @@ -150,6 +154,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { key: key, toolBarHeight: toolbarIconSize * 2, toolBarSectionSpacing: toolBarSectionSpacing, + toolBarIconAlignment: toolBarIconAlignment, multiRowsDisplay: multiRowsDisplay, locale: locale, children: [ @@ -413,6 +418,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { final List children; final double toolBarHeight; final double? toolBarSectionSpacing; + final ToolbarAlignment? toolBarIconAlignment; final bool? multiRowsDisplay; /// The color of the toolbar. @@ -440,7 +446,13 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { initialLocale: locale, child: multiRowsDisplay ?? true ? Wrap( - alignment: WrapAlignment.center, + alignment: + (toolBarIconAlignment==ToolbarAlignment.start) + ? WrapAlignment.start + :(toolBarIconAlignment==ToolbarAlignment.center) + ? WrapAlignment.center + :(toolBarIconAlignment==ToolbarAlignment.end) + ? WrapAlignment.end : WrapAlignment.center, runSpacing: 4, spacing: toolBarSectionSpacing ?? 4, children: children, From 0dedb1c805c90ed2c13dd5915982b3df3ac13cdc Mon Sep 17 00:00:00 2001 From: Cheryl Date: Sun, 14 Nov 2021 21:32:47 -0800 Subject: [PATCH 066/179] Upgrade to 2.0.17 --- CHANGELOG.md | 7 +++++-- lib/src/widgets/toolbar.dart | 14 +++++++------- pubspec.yaml | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c61d60eb..4ece66e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,14 @@ +## [2.0.17] +* Allow alignment of the toolbar icons to match WrapAlignment. + ## [2.0.16] * Add hide / show alignment buttons. ## [2.0.15] -* implement change cursor to SystemMouseCursors.click when hovering a link styled text. +* Implement change cursor to SystemMouseCursors.click when hovering a link styled text. ## [2.0.14] -* enable customize the checkbox widget using DefaultListBlockStyle style. +* Enable customize the checkbox widget using DefaultListBlockStyle style. ## [2.0.13] * Improve the scrolling performance by reducing the repaint areas. diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 80269eef..2e5899d6 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -49,7 +49,7 @@ typedef WebVideoPickImpl = Future Function( typedef MediaPickSettingSelector = Future Function( BuildContext context); -enum ToolbarAlignment{ start, center, end } +enum ToolbarAlignment { start, center, end } // The default size of the icon of a button. const double kDefaultIconSize = 18; @@ -446,13 +446,13 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { initialLocale: locale, child: multiRowsDisplay ?? true ? Wrap( - alignment: - (toolBarIconAlignment==ToolbarAlignment.start) + alignment: (toolBarIconAlignment == ToolbarAlignment.start) ? WrapAlignment.start - :(toolBarIconAlignment==ToolbarAlignment.center) - ? WrapAlignment.center - :(toolBarIconAlignment==ToolbarAlignment.end) - ? WrapAlignment.end : WrapAlignment.center, + : (toolBarIconAlignment == ToolbarAlignment.center) + ? WrapAlignment.center + : (toolBarIconAlignment == ToolbarAlignment.end) + ? WrapAlignment.end + : WrapAlignment.center, runSpacing: 4, spacing: toolBarSectionSpacing ?? 4, children: children, diff --git a/pubspec.yaml b/pubspec.yaml index f21ef84c..5da40094 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: 2.0.16 +version: 2.0.17 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 4cce50a6f917710f296d4a777113ce5ea0bee331 Mon Sep 17 00:00:00 2001 From: Cheryl Date: Sun, 14 Nov 2021 22:05:24 -0800 Subject: [PATCH 067/179] Make toolbar optional params not nullable --- lib/src/widgets/toolbar.dart | 42 +++++++++++++++--------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 2e5899d6..de38da0b 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -49,8 +49,6 @@ typedef WebVideoPickImpl = Future Function( typedef MediaPickSettingSelector = Future Function( BuildContext context); -enum ToolbarAlignment { start, center, end } - // The default size of the icon of a button. const double kDefaultIconSize = 18; @@ -60,12 +58,12 @@ const double kIconButtonFactor = 1.77; class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { const QuillToolbar({ required this.children, - this.toolBarHeight = 36, - this.toolBarSectionSpacing, - this.toolBarIconAlignment, + this.toolbarHeight = 36, + this.toolbarIconAlignment = WrapAlignment.center, + this.toolbarSectionSpacing = 4, + this.multiRowsDisplay = true, this.color, this.filePickImpl, - this.multiRowsDisplay, this.locale, Key? key, }) : super(key: key); @@ -73,8 +71,8 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { factory QuillToolbar.basic({ required QuillController controller, double toolbarIconSize = kDefaultIconSize, - double toolBarSectionSpacing = 4, - ToolbarAlignment toolBarIconAlignment = ToolbarAlignment.center, + double toolbarSectionSpacing = 4, + WrapAlignment toolbarIconAlignment = WrapAlignment.center, bool showBoldButton = true, bool showItalicButton = true, bool showSmallButton = false, @@ -152,9 +150,9 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { return QuillToolbar( key: key, - toolBarHeight: toolbarIconSize * 2, - toolBarSectionSpacing: toolBarSectionSpacing, - toolBarIconAlignment: toolBarIconAlignment, + toolbarHeight: toolbarIconSize * 2, + toolbarSectionSpacing: toolbarSectionSpacing, + toolbarIconAlignment: toolbarIconAlignment, multiRowsDisplay: multiRowsDisplay, locale: locale, children: [ @@ -416,10 +414,10 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { } final List children; - final double toolBarHeight; - final double? toolBarSectionSpacing; - final ToolbarAlignment? toolBarIconAlignment; - final bool? multiRowsDisplay; + final double toolbarHeight; + final double toolbarSectionSpacing; + final WrapAlignment toolbarIconAlignment; + final bool multiRowsDisplay; /// The color of the toolbar. /// @@ -438,23 +436,17 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { final Locale? locale; @override - Size get preferredSize => Size.fromHeight(toolBarHeight); + Size get preferredSize => Size.fromHeight(toolbarHeight); @override Widget build(BuildContext context) { return I18n( initialLocale: locale, - child: multiRowsDisplay ?? true + child: multiRowsDisplay ? Wrap( - alignment: (toolBarIconAlignment == ToolbarAlignment.start) - ? WrapAlignment.start - : (toolBarIconAlignment == ToolbarAlignment.center) - ? WrapAlignment.center - : (toolBarIconAlignment == ToolbarAlignment.end) - ? WrapAlignment.end - : WrapAlignment.center, + alignment: toolbarIconAlignment, runSpacing: 4, - spacing: toolBarSectionSpacing ?? 4, + spacing: toolbarSectionSpacing, children: children, ) : Container( From a724836a3935849ff9008bc352af5053db6ede73 Mon Sep 17 00:00:00 2001 From: mark8044 <87546778+mark8044@users.noreply.github.com> Date: Mon, 15 Nov 2021 13:30:44 -0800 Subject: [PATCH 068/179] Update toolbar.dart (#466) --- lib/src/widgets/toolbar.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index de38da0b..c8080c84 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -73,6 +73,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { double toolbarIconSize = kDefaultIconSize, double toolbarSectionSpacing = 4, WrapAlignment toolbarIconAlignment = WrapAlignment.center, + bool showDividers = true, bool showBoldButton = true, bool showItalicButton = true, bool showSmallButton = false, @@ -280,7 +281,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { webVideoPickImpl: webVideoPickImpl, iconTheme: iconTheme, ), - if (isButtonGroupShown[0] && + if (showDividers && isButtonGroupShown[0] && (isButtonGroupShown[1] || isButtonGroupShown[2] || isButtonGroupShown[3] || @@ -301,7 +302,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { showRightAlignment: showRightAlignment, showJustifyAlignment: showJustifyAlignment, ), - if (isButtonGroupShown[1] && + if (showDividers && isButtonGroupShown[1] && (isButtonGroupShown[2] || isButtonGroupShown[3] || isButtonGroupShown[4] || @@ -317,7 +318,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { iconSize: toolbarIconSize, iconTheme: iconTheme, ), - if (isButtonGroupShown[2] && + if (showDividers && isButtonGroupShown[2] && (isButtonGroupShown[3] || isButtonGroupShown[4] || isButtonGroupShown[5])) @@ -358,7 +359,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { iconSize: toolbarIconSize, iconTheme: iconTheme, ), - if (isButtonGroupShown[3] && + if (showDividers && isButtonGroupShown[3] && (isButtonGroupShown[4] || isButtonGroupShown[5])) VerticalDivider( indent: 12, @@ -389,7 +390,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { isIncrease: false, iconTheme: iconTheme, ), - if (isButtonGroupShown[4] && isButtonGroupShown[5]) + if (showDividers && isButtonGroupShown[4] && isButtonGroupShown[5]) VerticalDivider( indent: 12, endIndent: 12, From ce054fabdfd45f884951c697865a8c1697bb1d2d Mon Sep 17 00:00:00 2001 From: mark8044 <87546778+mark8044@users.noreply.github.com> Date: Mon, 15 Nov 2021 13:50:31 -0800 Subject: [PATCH 069/179] Update toolbar.dart (#467) --- lib/src/widgets/toolbar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index c8080c84..32e4987e 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -318,7 +318,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { iconSize: toolbarIconSize, iconTheme: iconTheme, ), - if (showDividers && isButtonGroupShown[2] && + if (showDividers && showHeaderStyle && isButtonGroupShown[2] && (isButtonGroupShown[3] || isButtonGroupShown[4] || isButtonGroupShown[5])) From 7c7ab448e3b1fdff706eba7e7273229aba0d55d8 Mon Sep 17 00:00:00 2001 From: Cheryl Date: Wed, 17 Nov 2021 19:18:56 -0800 Subject: [PATCH 070/179] Upgrade to 2.0.18 --- CHANGELOG.md | 3 +++ lib/src/widgets/toolbar.dart | 13 +++++++++---- pubspec.yaml | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ece66e4..ea83e903 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.18] +* Make toolbar dividers optional. + ## [2.0.17] * Allow alignment of the toolbar icons to match WrapAlignment. diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 32e4987e..7115ff2c 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -281,7 +281,8 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { webVideoPickImpl: webVideoPickImpl, iconTheme: iconTheme, ), - if (showDividers && isButtonGroupShown[0] && + if (showDividers && + isButtonGroupShown[0] && (isButtonGroupShown[1] || isButtonGroupShown[2] || isButtonGroupShown[3] || @@ -302,7 +303,8 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { showRightAlignment: showRightAlignment, showJustifyAlignment: showJustifyAlignment, ), - if (showDividers && isButtonGroupShown[1] && + if (showDividers && + isButtonGroupShown[1] && (isButtonGroupShown[2] || isButtonGroupShown[3] || isButtonGroupShown[4] || @@ -318,7 +320,9 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { iconSize: toolbarIconSize, iconTheme: iconTheme, ), - if (showDividers && showHeaderStyle && isButtonGroupShown[2] && + if (showDividers && + showHeaderStyle && + isButtonGroupShown[2] && (isButtonGroupShown[3] || isButtonGroupShown[4] || isButtonGroupShown[5])) @@ -359,7 +363,8 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { iconSize: toolbarIconSize, iconTheme: iconTheme, ), - if (showDividers && isButtonGroupShown[3] && + if (showDividers && + isButtonGroupShown[3] && (isButtonGroupShown[4] || isButtonGroupShown[5])) VerticalDivider( indent: 12, diff --git a/pubspec.yaml b/pubspec.yaml index 5da40094..2c9f219b 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: 2.0.17 +version: 2.0.18 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 6782a49b4d9874372330dc794f70ba3cdff4847e Mon Sep 17 00:00:00 2001 From: Develeste <93141030+Develeste@users.noreply.github.com> Date: Fri, 19 Nov 2021 00:02:52 +0900 Subject: [PATCH 071/179] When uploading a video, applying indicator (#469) --- lib/src/widgets/video_app.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/src/widgets/video_app.dart b/lib/src/widgets/video_app.dart index 44753a72..6d080f24 100644 --- a/lib/src/widgets/video_app.dart +++ b/lib/src/widgets/video_app.dart @@ -35,13 +35,15 @@ class _VideoAppState extends State { // Ensure the first frame is shown after the video is initialized, // even before the play button has been pressed. setState(() {}); - }); + }).catchError((error) { + setState(() {}); + });; } @override Widget build(BuildContext context) { final defaultStyles = DefaultStyles.getInstance(context); - if (!_controller.value.isInitialized || _controller.value.hasError) { + if (_controller.value.hasError) { if (widget.readOnly) { return RichText( text: TextSpan( @@ -54,6 +56,11 @@ class _VideoAppState extends State { return RichText( text: TextSpan(text: widget.videoUrl, style: defaultStyles.link)); + } else if (!_controller.value.isInitialized) { + return VideoProgressIndicator(_controller, + allowScrubbing: true, + colors: VideoProgressColors(playedColor: Colors.blue), + ); } return Container( From e0205a3cff570afa94e104a673fec6779cb4dabd Mon Sep 17 00:00:00 2001 From: X Code Date: Thu, 18 Nov 2021 07:51:52 -0800 Subject: [PATCH 072/179] Update README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 787bef8f..4d1a2243 100644 --- a/README.md +++ b/README.md @@ -126,10 +126,6 @@ Currently, translations are available for these locales: ### Contributing to translations The translation file is located at [lib/src/translations/toolbar.i18n.dart](lib/src/translations/toolbar.i18n.dart). Feel free to contribute your own translations, just copy the English translations map and replace the values with your translations. Then open a pull request so everyone can benefit from your translations! -## Migrate Zefyr Data - -Check out [code](https://github.com/jwehrle/zefyr_quill_convert) and [doc](https://docs.google.com/document/d/1FUSrpbarHnilb7uDN5J5DDahaI0v1RMXBjj4fFSpSuY/edit?usp=sharing). - ---

From b72a701e657b2babdf3626cd957d3eef7c424909 Mon Sep 17 00:00:00 2001 From: Cheryl Date: Thu, 18 Nov 2021 08:10:23 -0800 Subject: [PATCH 073/179] Upgrade to 2.0.19 --- CHANGELOG.md | 3 +++ lib/src/widgets/video_app.dart | 10 ++++++---- pubspec.yaml | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea83e903..e1d008b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.19] +* When uploading a video, applying indicator. + ## [2.0.18] * Make toolbar dividers optional. diff --git a/lib/src/widgets/video_app.dart b/lib/src/widgets/video_app.dart index 6d080f24..b0967451 100644 --- a/lib/src/widgets/video_app.dart +++ b/lib/src/widgets/video_app.dart @@ -37,7 +37,8 @@ class _VideoAppState extends State { setState(() {}); }).catchError((error) { setState(() {}); - });; + }); + ; } @override @@ -57,9 +58,10 @@ class _VideoAppState extends State { return RichText( text: TextSpan(text: widget.videoUrl, style: defaultStyles.link)); } else if (!_controller.value.isInitialized) { - return VideoProgressIndicator(_controller, - allowScrubbing: true, - colors: VideoProgressColors(playedColor: Colors.blue), + return VideoProgressIndicator( + _controller, + allowScrubbing: true, + colors: const VideoProgressColors(playedColor: Colors.blue), ); } diff --git a/pubspec.yaml b/pubspec.yaml index 2c9f219b..75a90c92 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: 2.0.18 +version: 2.0.19 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From f47301745c5a5a704afff8caa226ec98972eb1fa Mon Sep 17 00:00:00 2001 From: Develeste <93141030+Develeste@users.noreply.github.com> Date: Fri, 19 Nov 2021 01:42:48 +0900 Subject: [PATCH 074/179] Improving the UX/UI of Image widget (#470) --- lib/src/widgets/image.dart | 55 ++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/lib/src/widgets/image.dart b/lib/src/widgets/image.dart index b9df48ce..49b97c10 100644 --- a/lib/src/widgets/image.dart +++ b/lib/src/widgets/image.dart @@ -10,20 +10,59 @@ class ImageTapWrapper extends StatelessWidget { final ImageProvider? imageProvider; - @override +@override Widget build(BuildContext context) { return Scaffold( body: Container( constraints: BoxConstraints.expand( height: MediaQuery.of(context).size.height, ), - child: GestureDetector( - onTapDown: (_) { - Navigator.pop(context); - }, - child: PhotoView( - imageProvider: imageProvider, - ), + child: Stack( + children: [ + PhotoView( + imageProvider: imageProvider, + loadingBuilder: (context, event) { + return Container( + color: Colors.black, + child: Center( + child: CircularProgressIndicator(), + ), + ); + }, + ), + Positioned( + right: 10.0, + top: MediaQuery.of(context).padding.top + 10.0, + child: InkWell( + onTap: () { + Navigator.pop(context); + }, + child: Stack( + children: [ + Opacity( + opacity: 0.2, + child: Container( + height: 30.0, + width: 30.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.black87, + ), + ), + ), + Positioned( + top: 0, + bottom: 0, + left: 0, + right: 0, + child: Icon(Icons.close, + color: Colors.grey[400], size: 28.0), + ) + ], + ), + ), + ), + ], ), ), ); From c39cc6db7ee2bce8ece7a2b5a5b759ebef5b24d8 Mon Sep 17 00:00:00 2001 From: Cheryl Date: Thu, 18 Nov 2021 08:45:50 -0800 Subject: [PATCH 075/179] Upgrade to 2.0.20 --- CHANGELOG.md | 3 +++ lib/src/widgets/image.dart | 16 ++++++++-------- pubspec.yaml | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1d008b2..6ee25198 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.20] +* Improved UX/UI of Image widget. + ## [2.0.19] * When uploading a video, applying indicator. diff --git a/lib/src/widgets/image.dart b/lib/src/widgets/image.dart index 49b97c10..eb07e8db 100644 --- a/lib/src/widgets/image.dart +++ b/lib/src/widgets/image.dart @@ -10,7 +10,7 @@ class ImageTapWrapper extends StatelessWidget { final ImageProvider? imageProvider; -@override + @override Widget build(BuildContext context) { return Scaffold( body: Container( @@ -24,14 +24,14 @@ class ImageTapWrapper extends StatelessWidget { loadingBuilder: (context, event) { return Container( color: Colors.black, - child: Center( + child: const Center( child: CircularProgressIndicator(), ), ); }, ), Positioned( - right: 10.0, + right: 10, top: MediaQuery.of(context).padding.top + 10.0, child: InkWell( onTap: () { @@ -42,9 +42,9 @@ class ImageTapWrapper extends StatelessWidget { Opacity( opacity: 0.2, child: Container( - height: 30.0, - width: 30.0, - decoration: BoxDecoration( + height: 30, + width: 30, + decoration: const BoxDecoration( shape: BoxShape.circle, color: Colors.black87, ), @@ -55,8 +55,8 @@ class ImageTapWrapper extends StatelessWidget { bottom: 0, left: 0, right: 0, - child: Icon(Icons.close, - color: Colors.grey[400], size: 28.0), + child: + Icon(Icons.close, color: Colors.grey[400], size: 28), ) ], ), diff --git a/pubspec.yaml b/pubspec.yaml index 75a90c92..c158fcdc 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: 2.0.19 +version: 2.0.20 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From f6e1e2c0a8877cc8c4a8f5a65e80ad1127c6026a Mon Sep 17 00:00:00 2001 From: Tarekk Mohamed Abdalla Date: Thu, 18 Nov 2021 21:09:20 +0200 Subject: [PATCH 076/179] small rewording for the assert message (#471) --- lib/src/models/documents/nodes/embed.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/models/documents/nodes/embed.dart b/lib/src/models/documents/nodes/embed.dart index 18c550fe..66a3013f 100644 --- a/lib/src/models/documents/nodes/embed.dart +++ b/lib/src/models/documents/nodes/embed.dart @@ -19,7 +19,7 @@ class Embeddable { static Embeddable fromJson(Map json) { final m = Map.from(json); - assert(m.length == 1, 'Embeddable map has one key'); + assert(m.length == 1, 'Embeddable map must only have one key'); return BlockEmbed(m.keys.first, m.values.first); } From e652bf615e763c0b71afdc46978fa7c4797e2cb6 Mon Sep 17 00:00:00 2001 From: Slava Krampetz Date: Sat, 20 Nov 2021 11:17:10 +0700 Subject: [PATCH 077/179] Russian locale (#472) --- lib/src/translations/toolbar.i18n.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 3d7a3d71..6ae95fcd 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -57,6 +57,15 @@ extension Localization on String { 'Link': '링크', 'Please first select some text to transform into a link.': '링크로 전환할 글자를 먼저 선택해주세요.', + }, + 'ru': { + 'Paste a link': 'Вставить ссылку', + 'Ok': 'ОК', + 'Select Color': 'Выбрать цвет', + 'Gallery': 'Галерея', + 'Link': 'Ссылка', + 'Please first select some text to transform into a link.': + 'Выделите часть текста для создания ссылки.', } }; From f90606b5a26039c1334932803c3e480587340c99 Mon Sep 17 00:00:00 2001 From: X Code Date: Fri, 19 Nov 2021 20:19:56 -0800 Subject: [PATCH 078/179] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4d1a2243..b71c278b 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ Currently, translations are available for these locales: * `Locale('fr')` * `Locale('zh', 'CN')` * `Locale('ko')` +* `Locale('ru')` ### Contributing to translations The translation file is located at [lib/src/translations/toolbar.i18n.dart](lib/src/translations/toolbar.i18n.dart). Feel free to contribute your own translations, just copy the English translations map and replace the values with your translations. Then open a pull request so everyone can benefit from your translations! From eb9cc19e1742ee1f2671598923ecb974d9fe4c66 Mon Sep 17 00:00:00 2001 From: Viper5niper <43819949+Viper5niper@users.noreply.github.com> Date: Sun, 21 Nov 2021 17:44:22 -0600 Subject: [PATCH 079/179] added spanish (#475) --- lib/src/translations/toolbar.i18n.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 6ae95fcd..7bf13942 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -66,7 +66,16 @@ extension Localization on String { 'Link': 'Ссылка', 'Please first select some text to transform into a link.': 'Выделите часть текста для создания ссылки.', - } + }, + 'es': { + 'Paste a link': 'Pega un enlace', + 'Ok': 'Ok', + 'Select Color': 'Selecciona un color', + 'Gallery': 'Galeria', + 'Link': 'Enlace', + 'Please first select some text to transform into a link.': + 'Por favor selecciona primero un texto para transformarlo en un enlace', + }, }; String get i18n => localize(this, _t); From 0ad6150241296595d28fb6c606ec15e965b6911c Mon Sep 17 00:00:00 2001 From: X Code Date: Sun, 21 Nov 2021 15:51:02 -0800 Subject: [PATCH 080/179] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b71c278b..d6a19964 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ Currently, translations are available for these locales: * `Locale('zh', 'CN')` * `Locale('ko')` * `Locale('ru')` +* `Locale('es')` ### Contributing to translations The translation file is located at [lib/src/translations/toolbar.i18n.dart](lib/src/translations/toolbar.i18n.dart). Feel free to contribute your own translations, just copy the English translations map and replace the values with your translations. Then open a pull request so everyone can benefit from your translations! From ecc7382480f26ea9c194805acfd317abbbde8250 Mon Sep 17 00:00:00 2001 From: Malik Date: Tue, 23 Nov 2021 00:30:39 +0300 Subject: [PATCH 081/179] add turkish language (#476) * Update toolbar.i18n.dart turkish language * Update toolbar.i18n.dart turkish language --- lib/src/translations/toolbar.i18n.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 7bf13942..353b6c27 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -76,6 +76,15 @@ extension Localization on String { 'Please first select some text to transform into a link.': 'Por favor selecciona primero un texto para transformarlo en un enlace', }, + 'tr': { + 'Paste a link': 'Bağlantıyı Yapıştır', + 'Ok': 'Tamam', + 'Select Color': 'Renk Seçin', + 'Gallery': 'Galeri', + 'Link': 'Bağlantı', + 'Please first select some text to transform into a link.': + 'Lütfen bağlantıya dönüştürmek için bir metin seçin.', + }, }; String get i18n => localize(this, _t); From 9c84f5c0a6b2d6cb5ba4154218a9b4221f5c5b7e Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 22 Nov 2021 13:31:16 -0800 Subject: [PATCH 082/179] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d6a19964..d7efa693 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,7 @@ Currently, translations are available for these locales: * `Locale('ko')` * `Locale('ru')` * `Locale('es')` +* `Locale('tr')` ### Contributing to translations The translation file is located at [lib/src/translations/toolbar.i18n.dart](lib/src/translations/toolbar.i18n.dart). Feel free to contribute your own translations, just copy the English translations map and replace the values with your translations. Then open a pull request so everyone can benefit from your translations! From e954b3a6482027761facea9b56694ccb5946259b Mon Sep 17 00:00:00 2001 From: li3317 Date: Thu, 25 Nov 2021 15:01:14 -0500 Subject: [PATCH 083/179] handle click on embed object --- lib/src/translations/toolbar.i18n.dart | 5 +++-- lib/src/widgets/text_line.dart | 9 ++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 353b6c27..d8eba561 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -74,14 +74,15 @@ extension Localization on String { 'Gallery': 'Galeria', 'Link': 'Enlace', 'Please first select some text to transform into a link.': - 'Por favor selecciona primero un texto para transformarlo en un enlace', + 'Por favor selecciona primero un texto para transformarlo ' + 'en un enlace', }, 'tr': { 'Paste a link': 'Bağlantıyı Yapıştır', 'Ok': 'Tamam', 'Select Color': 'Renk Seçin', 'Gallery': 'Galeri', - 'Link': 'Bağlantı', + 'Link': 'Bağlantı', 'Please first select some text to transform into a link.': 'Lütfen bağlantıya dönüştürmek için bir metin seçin.', }, diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index e9e5f229..0990e797 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -876,7 +876,14 @@ class RenderEditableTextLine extends RenderEditableBox { @override bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { - return _children.first.hitTest(result, position: position); + if (_body == null) return false; + final parentData = _body!.parentData as BoxParentData; + return result.addWithPaintOffset( + offset: parentData.offset, + position: position, + hitTest: (result, position) { + return _body!.hitTest(result, position: position); + }); } @override From 65164f768f60a2956b4fb58f19025b34c60bc41a Mon Sep 17 00:00:00 2001 From: li3317 Date: Thu, 25 Nov 2021 22:42:39 -0500 Subject: [PATCH 084/179] upgrade to 2.0.21 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ee25198..d35c67ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.21] +* Handle click on embed object. + ## [2.0.20] * Improved UX/UI of Image widget. diff --git a/pubspec.yaml b/pubspec.yaml index c158fcdc..2b70bb4e 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: 2.0.20 +version: 2.0.21 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 06463721de46680ecfb1c5fb07354e155b04d086 Mon Sep 17 00:00:00 2001 From: Andy Trand Date: Mon, 29 Nov 2021 09:03:54 +0200 Subject: [PATCH 085/179] ukraine language support (#483) --- README.md | 1 + lib/src/translations/toolbar.i18n.dart | 9 +++++++++ lib/src/widgets/toolbar.dart | 2 ++ 3 files changed, 12 insertions(+) diff --git a/README.md b/README.md index d7efa693..d6b8f5d0 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ Currently, translations are available for these locales: * `Locale('ru')` * `Locale('es')` * `Locale('tr')` +* `Locale('uk')` ### Contributing to translations The translation file is located at [lib/src/translations/toolbar.i18n.dart](lib/src/translations/toolbar.i18n.dart). Feel free to contribute your own translations, just copy the English translations map and replace the values with your translations. Then open a pull request so everyone can benefit from your translations! diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index d8eba561..79a834c2 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -86,6 +86,15 @@ extension Localization on String { 'Please first select some text to transform into a link.': 'Lütfen bağlantıya dönüştürmek için bir metin seçin.', }, + 'uk': { + 'Paste a link': 'Вставити посилання', + 'Ok': 'ОК', + 'Select Color': 'Вибрати колір', + 'Gallery': 'Галерея', + 'Link': 'Посилання', + 'Please first select some text to transform into a link.': + 'Виділіть текст для створення посилання.', + }, }; String get i18n => localize(this, _t); diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 7115ff2c..68960fbf 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -122,6 +122,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { /// * Locale('de') /// * Locale('fr') /// * Locale('zh') + /// and more https://github.com/singerdmx/flutter-quill#translation-of-toolbar Locale? locale, Key? key, }) { @@ -439,6 +440,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { /// * Locale('de') /// * Locale('fr') /// * Locale('zh', 'CN') + /// and more https://github.com/singerdmx/flutter-quill#translation-of-toolbar final Locale? locale; @override From 477f3fc72f33409f8eb6ced37be09f0e733d3c89 Mon Sep 17 00:00:00 2001 From: Andy Trand Date: Mon, 29 Nov 2021 09:04:08 +0200 Subject: [PATCH 086/179] targetChild error temporary fix (#484) --- lib/src/widgets/editor.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 48524ca1..c67e2e8a 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -1192,7 +1192,11 @@ class RenderEditableContainerBox extends RenderBox if (targetChild.getContainer() == targetNode) { break; } - targetChild = childAfter(targetChild); + final newChild = childAfter(targetChild); + if (newChild == null) { + break; + } + targetChild = newChild; } if (targetChild == null) { throw 'targetChild should not be null'; From 1d5c7a5314f0d00a9508dca3087e37759d2ac480 Mon Sep 17 00:00:00 2001 From: Andy Trand Date: Mon, 29 Nov 2021 18:31:45 +0200 Subject: [PATCH 087/179] fix attributes compare (#486) --- lib/src/models/quill_delta.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/src/models/quill_delta.dart b/lib/src/models/quill_delta.dart index 47bbde2a..e4a11055 100644 --- a/lib/src/models/quill_delta.dart +++ b/lib/src/models/quill_delta.dart @@ -150,6 +150,11 @@ class Operation { /// Returns `true` if [other] operation has the same attributes as this one. bool hasSameAttributes(Operation other) { + // treat null and empty equal + if ((_attributes?.isEmpty ?? true) && + (other._attributes?.isEmpty ?? true)) { + return true; + } return _attributeEquality.equals(_attributes, other._attributes); } From b09de44e367713ce675ebe8762c63d3705d269ca Mon Sep 17 00:00:00 2001 From: Andy Trand Date: Mon, 29 Nov 2021 20:27:49 +0200 Subject: [PATCH 088/179] font size parsing fix (#487) --- lib/src/widgets/text_line.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index 0990e797..14546d11 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -230,7 +230,14 @@ class TextLine extends StatelessWidget { res = res.merge(defaultStyles.sizeHuge); break; default: - final fontSize = double.tryParse(size.value); + double? fontSize; + if (size.value is double) { + fontSize = size.value; + } else if (size.value is int) { + fontSize = size.value.toDouble(); + } else if (size.value is String) { + fontSize = double.tryParse(size.value); + } if (fontSize != null) { res = res.merge(TextStyle(fontSize: fontSize)); } else { From 9380ede2aea56efc4baa7c945be9c7768fb5080e Mon Sep 17 00:00:00 2001 From: li3317 Date: Mon, 29 Nov 2021 21:22:44 -0500 Subject: [PATCH 089/179] upgrade to 2.0.22 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d35c67ae..0ab8c3f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.22] +* Fix attribute compare and fix font size parsing. + ## [2.0.21] * Handle click on embed object. diff --git a/pubspec.yaml b/pubspec.yaml index 2b70bb4e..39947b72 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: 2.0.21 +version: 2.0.22 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 1b562c5b79aca49f3bf68dda15f8331c115c68c3 Mon Sep 17 00:00:00 2001 From: li3317 Date: Mon, 29 Nov 2021 21:52:09 -0500 Subject: [PATCH 090/179] reformat --- lib/src/translations/toolbar.i18n.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 79a834c2..0ddaf9e9 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -93,7 +93,7 @@ extension Localization on String { 'Gallery': 'Галерея', 'Link': 'Посилання', 'Please first select some text to transform into a link.': - 'Виділіть текст для створення посилання.', + 'Виділіть текст для створення посилання.', }, }; From b59e36009155b429883f9194c7481b41f8074a52 Mon Sep 17 00:00:00 2001 From: Andy Trand Date: Thu, 2 Dec 2021 20:21:55 +0200 Subject: [PATCH 091/179] Custom replaceText handler (#494) * ukraine language support * targetChild error temporary fix * fix attributes compare * font size parsing fix * manual replace text handler --- lib/src/widgets/controller.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index 15104add..d47dce72 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -10,11 +10,14 @@ import '../models/documents/style.dart'; import '../models/quill_delta.dart'; import '../utils/diff_delta.dart'; +typedef ReplaceTextCallback = bool Function(int index, int len, Object? data); + class QuillController extends ChangeNotifier { QuillController({ required this.document, required TextSelection selection, bool keepStyleOnNewLine = false, + this.onReplaceText, }) : _selection = selection, _keepStyleOnNewLine = keepStyleOnNewLine; @@ -36,6 +39,10 @@ class QuillController extends ChangeNotifier { TextSelection get selection => _selection; TextSelection _selection; + /// Manual [replaceText] handler + /// Return false to ignore the event + ReplaceTextCallback? onReplaceText; + /// Store any styles attribute that got toggled by the tap of a button /// and that has not been applied yet. /// It gets reset after each format action within the [document]. @@ -115,6 +122,10 @@ class QuillController extends ChangeNotifier { {bool ignoreFocus = false}) { assert(data is String || data is Embeddable); + if (onReplaceText != null && !onReplaceText!(index, len, data)) { + return; + } + Delta? delta; if (len > 0 || data is! String || data.isNotEmpty) { delta = document.replace(index, len, data); From d23e161d1fd2027be0f2f4b7c3cf7e3a82f9ceca Mon Sep 17 00:00:00 2001 From: Andy Trand Date: Fri, 3 Dec 2021 00:43:57 +0200 Subject: [PATCH 092/179] style fetch fix on editing between different blocks (#496) --- lib/src/models/documents/nodes/container.dart | 2 +- .../raw_editor_state_text_input_client_mixin.dart | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/src/models/documents/nodes/container.dart b/lib/src/models/documents/nodes/container.dart index 40d3ba9e..ac13ff27 100644 --- a/lib/src/models/documents/nodes/container.dart +++ b/lib/src/models/documents/nodes/container.dart @@ -94,7 +94,7 @@ abstract class Container extends Node { for (final node in children) { final len = node.length; - if (offset < len || (inclusive && offset == len && node.isLast)) { + if (offset < len || (inclusive && offset == len)) { return ChildQuery(node, offset); } offset -= len; diff --git a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart index bfb36106..6e4607f9 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import '../../models/documents/document.dart'; import '../../utils/diff_delta.dart'; import '../editor.dart'; @@ -167,8 +168,12 @@ mixin RawEditorStateTextInputClientMixin on EditorState 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); + if (diff.deleted.isEmpty && diff.inserted.isEmpty) { + widget.controller.updateSelection(value.selection, ChangeSource.LOCAL); + } else { + widget.controller.replaceText( + diff.start, diff.deleted.length, diff.inserted, value.selection); + } } @override From cdd48723869452e8563c3979cba6269b002bf143 Mon Sep 17 00:00:00 2001 From: li3317 Date: Thu, 2 Dec 2021 22:52:56 -0500 Subject: [PATCH 093/179] upgrade to 2.0.23 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ab8c3f3..391ce2f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.23] +* Support custom replaceText handler. + ## [2.0.22] * Fix attribute compare and fix font size parsing. diff --git a/pubspec.yaml b/pubspec.yaml index 39947b72..281475e1 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: 2.0.22 +version: 2.0.23 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From f14015c6d6d25a929f2720a23aa5cd0878d6e3c5 Mon Sep 17 00:00:00 2001 From: X Code Date: Sat, 4 Dec 2021 18:09:20 -0800 Subject: [PATCH 094/179] Add ScriptAttribute --- lib/src/models/documents/attribute.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/src/models/documents/attribute.dart b/lib/src/models/documents/attribute.dart index db667ce1..ecf05e26 100644 --- a/lib/src/models/documents/attribute.dart +++ b/lib/src/models/documents/attribute.dart @@ -39,6 +39,7 @@ class Attribute { Attribute.height.key: Attribute.height, Attribute.style.key: Attribute.style, Attribute.token.key: Attribute.token, + Attribute.script.key: Attribute.script, }); static final BoldAttribute bold = BoldAttribute(); @@ -85,6 +86,8 @@ class Attribute { static final TokenAttribute token = TokenAttribute(''); + static final ScriptAttribute script = ScriptAttribute(''); + static final Set inlineKeys = { Attribute.bold.key, Attribute.italic.key, @@ -312,3 +315,8 @@ class StyleAttribute extends Attribute { class TokenAttribute extends Attribute { TokenAttribute(String val) : super('token', AttributeScope.IGNORE, val); } + +// `script` is supposed to be inline attribute but it is not supported yet +class ScriptAttribute extends Attribute { + ScriptAttribute(String val) : super('script', AttributeScope.IGNORE, val); +} From d1fe52f6cb64ef0568c1bee589cbccf2d654b7eb Mon Sep 17 00:00:00 2001 From: Michael Thomsen Date: Mon, 6 Dec 2021 16:31:23 +0100 Subject: [PATCH 095/179] Update toolbar.i18n.dart (#499) --- lib/src/translations/toolbar.i18n.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 0ddaf9e9..8984da73 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -21,6 +21,15 @@ extension Localization on String { 'Please first select some text to transform into a link.': 'يرجى اختيار نص للتحويل إلى رابط', }, + 'da': { + 'Paste a link': 'Indsæt link', + 'Ok': 'Ok', + 'Select Color': 'Vælg farve', + 'Gallery': 'Galleri', + 'Link': 'Link', + 'Please first select some text to transform into a link.': + 'Vælg venligst først noget tekst for at lave det om til et link.', + }, 'de': { 'Paste a link': 'Link hinzufügen', 'Ok': 'Ok', From 35c9f6bb0ee6e3c6d3d8d38c0ab54035ad438c7e Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 6 Dec 2021 08:41:35 -0800 Subject: [PATCH 096/179] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d6b8f5d0..aea1bf50 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ Currently, translations are available for these locales: * `Locale('en')` * `Locale('ar')` * `Locale('de')` +* `Locale('da')` * `Locale('fr')` * `Locale('zh', 'CN')` * `Locale('ko')` From 4694189a6c6af65c6a04cab0928c2d901c7f8e9e Mon Sep 17 00:00:00 2001 From: Andy Trand Date: Tue, 7 Dec 2021 12:52:50 +0200 Subject: [PATCH 097/179] add delete handler (#501) --- lib/src/widgets/controller.dart | 13 ++++++++++++- .../raw_editor_state_keyboard_mixin.dart | 16 ++++++++++------ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index d47dce72..bb092646 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -11,6 +11,7 @@ import '../models/quill_delta.dart'; import '../utils/diff_delta.dart'; typedef ReplaceTextCallback = bool Function(int index, int len, Object? data); +typedef DeleteCallback = void Function(int cursorPosition, bool forward); class QuillController extends ChangeNotifier { QuillController({ @@ -18,6 +19,7 @@ class QuillController extends ChangeNotifier { required TextSelection selection, bool keepStyleOnNewLine = false, this.onReplaceText, + this.onDelete, }) : _selection = selection, _keepStyleOnNewLine = keepStyleOnNewLine; @@ -39,10 +41,13 @@ class QuillController extends ChangeNotifier { TextSelection get selection => _selection; TextSelection _selection; - /// Manual [replaceText] handler + /// Custom [replaceText] handler /// Return false to ignore the event ReplaceTextCallback? onReplaceText; + /// Custom delete handler + DeleteCallback? onDelete; + /// Store any styles attribute that got toggled by the tap of a button /// and that has not been applied yet. /// It gets reset after each format action within the [document]. @@ -186,6 +191,12 @@ class QuillController extends ChangeNotifier { ignoreFocusOnTextChange = false; } + /// Called in two cases: + /// forward == false && textBefore.isEmpty + /// forward == true && textAfter.isEmpty + void handleDelete(int cursorPosition, bool forward) => + onDelete?.call(cursorPosition, forward); + void formatText(int index, int len, Attribute? attribute) { if (len == 0 && attribute!.isInline && diff --git a/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart index db860248..72fd5dc2 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart @@ -153,12 +153,16 @@ mixin RawEditorStateKeyboardMixin on EditorState { final newSelection = TextSelection.collapsed(offset: cursorPosition); final newText = textBefore + textAfter; final size = plainText.length - newText.length; - widget.controller.replaceText( - cursorPosition, - size, - '', - newSelection, - ); + if (size == 0) { + widget.controller.handleDelete(cursorPosition, forward); + } else { + widget.controller.replaceText( + cursorPosition, + size, + '', + newSelection, + ); + } } TextSelection _jumpToBeginOrEndOfWord( From 931f7e65137ac977a5a884c8d587f93c4c14a695 Mon Sep 17 00:00:00 2001 From: Andy Trand Date: Tue, 7 Dec 2021 12:53:13 +0200 Subject: [PATCH 098/179] remove newline on concat (#502) --- lib/src/models/quill_delta.dart | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/src/models/quill_delta.dart b/lib/src/models/quill_delta.dart index e4a11055..3cab1910 100644 --- a/lib/src/models/quill_delta.dart +++ b/lib/src/models/quill_delta.dart @@ -608,9 +608,28 @@ class Delta { } } + /// Removes trailing '\n' + void _trimNewLine() { + if (isNotEmpty) { + final lastOp = _operations.last; + final lastOpData = lastOp.data; + + if (lastOpData is String && lastOpData.endsWith('\n')) { + _operations.removeLast(); + if (lastOpData.length > 1) { + insert(lastOpData.substring(0, lastOpData.length - 1), + lastOp.attributes); + } + } + } + } + /// Concatenates [other] with this delta and returns the result. - Delta concat(Delta other) { + Delta concat(Delta other, {bool trimNewLine = false}) { final result = Delta.from(this); + if (trimNewLine) { + result._trimNewLine(); + } if (other.isNotEmpty) { // In case first operation of other can be merged with last operation in // our list. From e265b54b1bcfcd696f81307161b7edf7b6386778 Mon Sep 17 00:00:00 2001 From: X Code Date: Tue, 7 Dec 2021 03:18:13 -0800 Subject: [PATCH 099/179] Upgrade to 2.1.0. --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 391ce2f1..0704a47e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.1.0] +* Add delete handler + ## [2.0.23] * Support custom replaceText handler. diff --git a/pubspec.yaml b/pubspec.yaml index 281475e1..0ed5e01b 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: 2.0.23 +version: 2.1.0 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 7d6319de5f9adc82188c9c0f22141438e5a3ecd6 Mon Sep 17 00:00:00 2001 From: Andy Trand Date: Tue, 7 Dec 2021 17:56:18 +0200 Subject: [PATCH 100/179] clear editor (#505) --- lib/src/widgets/controller.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index bb092646..8e6a3a92 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -122,6 +122,13 @@ class QuillController extends ChangeNotifier { bool get hasRedo => document.hasRedo; + /// clear editor + void clear() { + replaceText( + 0, plainTextEditingValue.text.length-1, '', + TextSelection.collapsed(offset: 0)); + } + void replaceText( int index, int len, Object? data, TextSelection? textSelection, {bool ignoreFocus = false}) { From 9f689306956f202e759f4c850a90db89c0d164fb Mon Sep 17 00:00:00 2001 From: Andy Trand Date: Tue, 7 Dec 2021 17:57:09 +0200 Subject: [PATCH 101/179] move cursor helpers (#507) --- lib/src/widgets/controller.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index 8e6a3a92..db211d1d 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -225,6 +225,16 @@ class QuillController extends ChangeNotifier { formatText(selection.start, selection.end - selection.start, attribute); } + void moveCursorToStart() { + updateSelection(TextSelection.collapsed(offset: 0), ChangeSource.LOCAL); + } + + void moveCursorToEnd() { + updateSelection( + TextSelection.collapsed(offset: plainTextEditingValue.text.length), + ChangeSource.LOCAL); + } + void updateSelection(TextSelection textSelection, ChangeSource source) { _updateSelection(textSelection, source); notifyListeners(); From 1b44569436bdd549e504bd41f457b3df8284fa11 Mon Sep 17 00:00:00 2001 From: X Code Date: Tue, 7 Dec 2021 08:17:30 -0800 Subject: [PATCH 102/179] Upgrade to 2.1.1 --- CHANGELOG.md | 5 ++++- lib/src/widgets/controller.dart | 8 ++++---- pubspec.yaml | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0704a47e..006f50f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ +## [2.1.1] +* Add methods of clearing editor and moving cursor. + ## [2.1.0] -* Add delete handler +* Add delete handler. ## [2.0.23] * Support custom replaceText handler. diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index db211d1d..d389cc3d 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -124,9 +124,8 @@ class QuillController extends ChangeNotifier { /// clear editor void clear() { - replaceText( - 0, plainTextEditingValue.text.length-1, '', - TextSelection.collapsed(offset: 0)); + replaceText(0, plainTextEditingValue.text.length - 1, '', + const TextSelection.collapsed(offset: 0)); } void replaceText( @@ -226,7 +225,8 @@ class QuillController extends ChangeNotifier { } void moveCursorToStart() { - updateSelection(TextSelection.collapsed(offset: 0), ChangeSource.LOCAL); + updateSelection( + const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL); } void moveCursorToEnd() { diff --git a/pubspec.yaml b/pubspec.yaml index 0ed5e01b..21febb2a 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: 2.1.0 +version: 2.1.1 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 2db6f59dac8772b2902e222bab5b1c9786e06a5f Mon Sep 17 00:00:00 2001 From: li3317 Date: Tue, 7 Dec 2021 22:32:38 -0500 Subject: [PATCH 103/179] use named constructor for raw editor --- lib/src/widgets/editor.dart | 92 +++++++++++++++++---------------- lib/src/widgets/raw_editor.dart | 48 +++++++++-------- 2 files changed, 73 insertions(+), 67 deletions(-) diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index c67e2e8a..4d344d5f 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -145,7 +145,7 @@ String _standardizeImageUrl(String url) { bool _isMobile() => io.Platform.isAndroid || io.Platform.isIOS; -Widget _defaultEmbedBuilder( +Widget defaultEmbedBuilder( BuildContext context, leaf.Embed node, bool readOnly) { assert(!kIsWeb, 'Please provide EmbedBuilder for Web'); switch (node.value.type) { @@ -251,7 +251,7 @@ class QuillEditor extends StatefulWidget { this.onSingleLongTapStart, this.onSingleLongTapMoveUpdate, this.onSingleLongTapEnd, - this.embedBuilder = _defaultEmbedBuilder, + this.embedBuilder = defaultEmbedBuilder, this.customStyleBuilder, Key? key}); @@ -377,51 +377,53 @@ class _QuillEditorState extends State throw UnimplementedError(); } + final child = RawEditor( + key: _editorKey, + controller: widget.controller, + focusNode: widget.focusNode, + scrollController: widget.scrollController, + scrollable: widget.scrollable, + scrollBottomInset: widget.scrollBottomInset, + padding: widget.padding, + readOnly: widget.readOnly, + placeholder: widget.placeholder, + onLaunchUrl: widget.onLaunchUrl, + toolbarOptions: ToolbarOptions( + copy: widget.enableInteractiveSelection, + cut: widget.enableInteractiveSelection, + paste: widget.enableInteractiveSelection, + selectAll: widget.enableInteractiveSelection, + ), + showSelectionHandles: theme.platform == TargetPlatform.iOS || + theme.platform == TargetPlatform.android, + showCursor: widget.showCursor, + cursorStyle: CursorStyle( + color: cursorColor, + backgroundColor: Colors.grey, + width: 2, + radius: cursorRadius, + offset: cursorOffset, + paintAboveText: widget.paintCursorAboveText ?? paintCursorAboveText, + opacityAnimates: cursorOpacityAnimates, + ), + textCapitalization: widget.textCapitalization, + minHeight: widget.minHeight, + maxHeight: widget.maxHeight, + customStyles: widget.customStyles, + expands: widget.expands, + autoFocus: widget.autoFocus, + selectionColor: selectionColor, + selectionCtrls: textSelectionControls, + keyboardAppearance: widget.keyboardAppearance, + enableInteractiveSelection: widget.enableInteractiveSelection, + scrollPhysics: widget.scrollPhysics, + embedBuilder: widget.embedBuilder, + customStyleBuilder: widget.customStyleBuilder, + ); + return _selectionGestureDetectorBuilder.build( HitTestBehavior.translucent, - RawEditor( - _editorKey, - widget.controller, - widget.focusNode, - widget.scrollController, - widget.scrollable, - widget.scrollBottomInset, - widget.padding, - widget.readOnly, - widget.placeholder, - widget.onLaunchUrl, - ToolbarOptions( - copy: widget.enableInteractiveSelection, - cut: widget.enableInteractiveSelection, - paste: widget.enableInteractiveSelection, - selectAll: widget.enableInteractiveSelection, - ), - theme.platform == TargetPlatform.iOS || - theme.platform == TargetPlatform.android, - widget.showCursor, - CursorStyle( - color: cursorColor, - backgroundColor: Colors.grey, - width: 2, - radius: cursorRadius, - offset: cursorOffset, - paintAboveText: widget.paintCursorAboveText ?? paintCursorAboveText, - opacityAnimates: cursorOpacityAnimates, - ), - widget.textCapitalization, - widget.maxHeight, - widget.minHeight, - widget.customStyles, - widget.expands, - widget.autoFocus, - selectionColor, - textSelectionControls, - widget.keyboardAppearance, - widget.enableInteractiveSelection, - widget.scrollPhysics, - widget.embedBuilder, - widget.customStyleBuilder, - ), + child, ); } diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index e9502678..e4e3f312 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -32,41 +32,45 @@ import 'text_line.dart'; import 'text_selection.dart'; class RawEditor extends StatefulWidget { - const RawEditor( - Key key, - this.controller, - this.focusNode, - this.scrollController, - this.scrollable, - this.scrollBottomInset, - this.padding, - this.readOnly, + const RawEditor({ + required this.controller, + required this.focusNode, + required this.scrollController, + required this.scrollBottomInset, + required this.cursorStyle, + required this.selectionColor, + required this.selectionCtrls, + Key? key, + this.scrollable = true, + this.padding = EdgeInsets.zero, + this.readOnly = false, this.placeholder, this.onLaunchUrl, - this.toolbarOptions, - this.showSelectionHandles, + this.toolbarOptions = const ToolbarOptions( + copy: true, + cut: true, + paste: true, + selectAll: true, + ), + this.showSelectionHandles = false, bool? showCursor, - this.cursorStyle, - this.textCapitalization, + this.textCapitalization = TextCapitalization.none, this.maxHeight, this.minHeight, this.customStyles, - this.expands, - this.autoFocus, - this.selectionColor, - this.selectionCtrls, - this.keyboardAppearance, - this.enableInteractiveSelection, + this.expands = false, + this.autoFocus = false, + this.keyboardAppearance = Brightness.light, + this.enableInteractiveSelection = true, this.scrollPhysics, - this.embedBuilder, + this.embedBuilder = defaultEmbedBuilder, this.customStyleBuilder, - ) : assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'), + }) : assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'), assert(minHeight == null || minHeight >= 0, 'minHeight cannot be null'), assert(maxHeight == null || minHeight == null || maxHeight >= minHeight, 'maxHeight cannot be null'), showCursor = showCursor ?? true, super(key: key); - final QuillController controller; final FocusNode focusNode; final ScrollController scrollController; From f9bcaa641236ed782587dd99758531340b49a6ba Mon Sep 17 00:00:00 2001 From: X Code Date: Tue, 7 Dec 2021 19:42:39 -0800 Subject: [PATCH 104/179] Refactor out getAlignment method --- lib/src/utils/string_helper.dart | 36 ++++++++++++++++++++++++++++++++ lib/src/widgets/editor.dart | 28 +------------------------ 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/lib/src/utils/string_helper.dart b/lib/src/utils/string_helper.dart index 44abfd8a..e3d8b7e0 100644 --- a/lib/src/utils/string_helper.dart +++ b/lib/src/utils/string_helper.dart @@ -1,3 +1,5 @@ +import 'package:flutter/cupertino.dart'; + Map parseKeyValuePairs(String s, Set targetKeys) { final result = {}; final pairs = s.split(';'); @@ -14,3 +16,37 @@ Map parseKeyValuePairs(String s, Set targetKeys) { return result; } + +Alignment getAlignment(String? s) { + const _defaultAlignment = Alignment.center; + if (s == null) { + return _defaultAlignment; + } + + final _index = [ + 'topLeft', + 'topCenter', + 'topRight', + 'centerLeft', + 'center', + 'centerRight', + 'bottomLeft', + 'bottomCenter', + 'bottomRight' + ].indexOf(s); + if (_index < 0) { + return _defaultAlignment; + } + + return [ + Alignment.topLeft, + Alignment.topCenter, + Alignment.topRight, + Alignment.centerLeft, + Alignment.center, + Alignment.centerRight, + Alignment.bottomLeft, + Alignment.bottomCenter, + Alignment.bottomRight + ][_index]; +} diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 4d344d5f..7cf198e7 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -165,33 +165,7 @@ Widget defaultEmbedBuilder( final m = _attrs['mobileMargin'] == null ? 0.0 : double.parse(_attrs['mobileMargin']!); - var a = Alignment.center; - if (_attrs['mobileAlignment'] != null) { - final _index = [ - 'topLeft', - 'topCenter', - 'topRight', - 'centerLeft', - 'center', - 'centerRight', - 'bottomLeft', - 'bottomCenter', - 'bottomRight' - ].indexOf(_attrs['mobileAlignment']!); - if (_index >= 0) { - a = [ - Alignment.topLeft, - Alignment.topCenter, - Alignment.topRight, - Alignment.centerLeft, - Alignment.center, - Alignment.centerRight, - Alignment.bottomLeft, - Alignment.bottomCenter, - Alignment.bottomRight - ][_index]; - } - } + final a = getAlignment(_attrs['mobileAlignment']); return Padding( padding: EdgeInsets.all(m), child: imageUrl.startsWith('http') From 881372dd13c1f7daab12cdca84260880a2861c59 Mon Sep 17 00:00:00 2001 From: Andy Trand Date: Thu, 9 Dec 2021 23:21:53 +0200 Subject: [PATCH 105/179] selection delegate methods, imports cleanup (#515) --- lib/src/utils/color.dart | 2 - lib/src/widgets/controller.dart | 2 + lib/src/widgets/default_styles.dart | 1 - lib/src/widgets/delegate.dart | 3 - lib/src/widgets/editor.dart | 5 +- lib/src/widgets/image.dart | 2 - lib/src/widgets/raw_editor.dart | 5 +- .../raw_editor_state_keyboard_mixin.dart | 9 +- ...editor_state_selection_delegate_mixin.dart | 131 ++++++++++++++++-- ..._editor_state_text_input_client_mixin.dart | 1 - lib/src/widgets/simple_viewer.dart | 1 - lib/src/widgets/text_block.dart | 1 - lib/src/widgets/text_selection.dart | 2 - lib/src/widgets/toolbar/camera_button.dart | 2 - .../widgets/toolbar/clear_format_button.dart | 1 - lib/src/widgets/toolbar/color_button.dart | 1 - lib/src/widgets/toolbar/history_button.dart | 1 - lib/src/widgets/toolbar/image_button.dart | 2 - .../widgets/toolbar/image_video_utils.dart | 1 - lib/src/widgets/toolbar/indent_button.dart | 1 - .../widgets/toolbar/insert_embed_button.dart | 1 - .../widgets/toolbar/link_style_button.dart | 1 - .../toolbar/select_alignment_button.dart | 2 +- .../toolbar/select_header_style_button.dart | 2 +- .../toolbar/toggle_check_list_button.dart | 1 - .../widgets/toolbar/toggle_style_button.dart | 1 - lib/src/widgets/toolbar/video_button.dart | 2 - 27 files changed, 136 insertions(+), 48 deletions(-) diff --git a/lib/src/utils/color.dart b/lib/src/utils/color.dart index 93b6e12b..dc3b5fa5 100644 --- a/lib/src/utils/color.dart +++ b/lib/src/utils/color.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; Color stringToColor(String? s) { diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index d389cc3d..5ef115c8 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -200,6 +200,8 @@ class QuillController extends ChangeNotifier { /// Called in two cases: /// forward == false && textBefore.isEmpty /// forward == true && textAfter.isEmpty + /// Android only + /// see https://github.com/singerdmx/flutter-quill/discussions/514 void handleDelete(int cursorPosition, bool forward) => onDelete?.call(cursorPosition, forward); diff --git a/lib/src/widgets/default_styles.dart b/lib/src/widgets/default_styles.dart index 79d5f55a..af1469c1 100644 --- a/lib/src/widgets/default_styles.dart +++ b/lib/src/widgets/default_styles.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:tuple/tuple.dart'; import 'style_widgets/style_widgets.dart'; diff --git a/lib/src/widgets/delegate.dart b/lib/src/widgets/delegate.dart index fa03c9a6..14adb9a9 100644 --- a/lib/src/widgets/delegate.dart +++ b/lib/src/widgets/delegate.dart @@ -1,11 +1,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import '../../flutter_quill.dart'; -import '../models/documents/nodes/leaf.dart'; -import 'editor.dart'; import 'text_selection.dart'; typedef EmbedBuilder = Widget Function( diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 7cf198e7..c12c314d 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -7,7 +7,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.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:url_launcher/url_launcher.dart'; @@ -51,7 +50,7 @@ abstract class EditorState extends State { TextEditingValue getTextEditingValue(); - void setTextEditingValue(TextEditingValue value); + void setTextEditingValue(TextEditingValue value, SelectionChangedCause cause); RenderEditor? getRenderEditor(); @@ -62,6 +61,8 @@ abstract class EditorState extends State { void hideToolbar(); void requestKeyboard(); + + bool get readOnly; } /// Base interface for editable render objects. diff --git a/lib/src/widgets/image.dart b/lib/src/widgets/image.dart index eb07e8db..3d09bd74 100644 --- a/lib/src/widgets/image.dart +++ b/lib/src/widgets/image.dart @@ -1,6 +1,4 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:photo_view/photo_view.dart'; class ImageTapWrapper extends StatelessWidget { diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index e4e3f312..1a4f3b6a 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -641,7 +641,8 @@ class RawEditorState extends EditorState } @override - void setTextEditingValue(TextEditingValue value) { + void setTextEditingValue( + TextEditingValue value, SelectionChangedCause cause) { if (value.text == textEditingValue.text) { widget.controller.updateSelection(value.selection, ChangeSource.LOCAL); } else { @@ -723,6 +724,8 @@ class RawEditorState extends EditorState @override bool get wantKeepAlive => widget.focusNode.hasFocus; + + bool get readOnly => widget.readOnly; } class _Editor extends MultiChildRenderObjectWidget { diff --git a/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart index 72fd5dc2..8156e6ee 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart @@ -104,7 +104,7 @@ mixin RawEditorStateKeyboardMixin on EditorState { text: selection.textBefore(plainText) + selection.textAfter(plainText), selection: TextSelection.collapsed(offset: selection.start), - )); + ), SelectionChangedCause.keyboard); } return; } @@ -156,12 +156,7 @@ mixin RawEditorStateKeyboardMixin on EditorState { if (size == 0) { widget.controller.handleDelete(cursorPosition, forward); } else { - widget.controller.replaceText( - cursorPosition, - size, - '', - newSelection, - ); + widget.controller.replaceText(cursorPosition, size, '', newSelection); } } 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 dcb9809a..8f29c2b7 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 @@ -1,6 +1,8 @@ -import 'dart:math'; +import 'dart:math' as math; +import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import '../editor.dart'; @@ -14,7 +16,8 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState @override set textEditingValue(TextEditingValue value) { - setTextEditingValue(value); + // deprecated + setTextEditingValue(value, SelectionChangedCause.keyboard); } @override @@ -50,8 +53,8 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState final expandedRect = Rect.fromCenter( center: rect.center, width: rect.width, - height: - max(rect.height, getRenderEditor()!.preferredLineHeight(position)), + height: math.max( + rect.height, getRenderEditor()!.preferredLineHeight(position)), ); additionalOffset = expandedRect.height >= editableSize.height @@ -81,10 +84,8 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState @override void userUpdateTextEditingValue( - TextEditingValue value, - SelectionChangedCause cause, - ) { - setTextEditingValue(value); + TextEditingValue value, SelectionChangedCause cause) { + setTextEditingValue(value, cause); } @override @@ -98,4 +99,118 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState @override bool get selectAllEnabled => widget.toolbarOptions.selectAll; + + void setSelection(TextSelection nextSelection, SelectionChangedCause cause) { + if (nextSelection == textEditingValue.selection) { + return; + } + setTextEditingValue( + textEditingValue.copyWith(selection: nextSelection), + cause, + ); + } + + @override + void copySelection(SelectionChangedCause cause) { + final selection = textEditingValue.selection; + if (selection.isCollapsed || !selection.isValid) { + return; + } + Clipboard.setData( + ClipboardData(text: selection.textInside(textEditingValue.text))); + + if (cause == SelectionChangedCause.toolbar) { + bringIntoView(textEditingValue.selection.extent); + hideToolbar(false); + + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + break; + case TargetPlatform.macOS: + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + // Collapse the selection and hide the toolbar and handles. + userUpdateTextEditingValue( + TextEditingValue( + text: textEditingValue.text, + selection: TextSelection.collapsed(offset: textEditingValue.selection.end), + ), + SelectionChangedCause.toolbar, + ); + break; + } + } + } + + @override + void cutSelection(SelectionChangedCause cause) { + final selection = textEditingValue.selection; + if (readOnly || !selection.isValid || selection.isCollapsed) { + return; + } + final text = textEditingValue.text; + Clipboard.setData(ClipboardData(text: selection.textInside(text))); + setTextEditingValue( + TextEditingValue( + text: selection.textBefore(text) + selection.textAfter(text), + selection: TextSelection.collapsed( + offset: math.min(selection.start, selection.end), + affinity: selection.affinity, + ), + ), + cause, + ); + + if (cause == SelectionChangedCause.toolbar) { + bringIntoView(textEditingValue.selection.extent); + hideToolbar(); + } + } + + @override + Future pasteText(SelectionChangedCause cause) async { + final selection = textEditingValue.selection; + if (readOnly || !selection.isValid) { + return; + } + final text = textEditingValue.text; + // See https://github.com/flutter/flutter/issues/11427 + final data = await Clipboard.getData(Clipboard.kTextPlain); + if (data == null) { + return; + } + setTextEditingValue( + TextEditingValue( + text: + selection.textBefore(text) + data.text! + selection.textAfter(text), + selection: TextSelection.collapsed( + offset: math.min(selection.start, selection.end) + data.text!.length, + affinity: selection.affinity, + ), + ), + cause, + ); + + if (cause == SelectionChangedCause.toolbar) { + bringIntoView(textEditingValue.selection.extent); + hideToolbar(); + } + } + + @override + void selectAll(SelectionChangedCause cause) { + setSelection( + textEditingValue.selection.copyWith( + baseOffset: 0, + extentOffset: textEditingValue.text.length, + ), + cause, + ); + + if (cause == SelectionChangedCause.toolbar) { + bringIntoView(textEditingValue.selection.extent); + } + } } diff --git a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart index 6e4607f9..37c5ece1 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart @@ -1,6 +1,5 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import '../../models/documents/document.dart'; import '../../utils/diff_delta.dart'; diff --git a/lib/src/widgets/simple_viewer.dart b/lib/src/widgets/simple_viewer.dart index dc68fe2a..86dcd4f3 100644 --- a/lib/src/widgets/simple_viewer.dart +++ b/lib/src/widgets/simple_viewer.dart @@ -4,7 +4,6 @@ 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'; diff --git a/lib/src/widgets/text_block.dart b/lib/src/widgets/text_block.dart index 40fc351b..245cd332 100644 --- a/lib/src/widgets/text_block.dart +++ b/lib/src/widgets/text_block.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:tuple/tuple.dart'; diff --git a/lib/src/widgets/text_selection.dart b/lib/src/widgets/text_selection.dart index 16374789..593a8135 100644 --- a/lib/src/widgets/text_selection.dart +++ b/lib/src/widgets/text_selection.dart @@ -1,11 +1,9 @@ import 'dart:async'; import 'dart:math' as math; -import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import '../models/documents/nodes/node.dart'; diff --git a/lib/src/widgets/toolbar/camera_button.dart b/lib/src/widgets/toolbar/camera_button.dart index 7686e47c..96776c39 100644 --- a/lib/src/widgets/toolbar/camera_button.dart +++ b/lib/src/widgets/toolbar/camera_button.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; @@ -6,7 +5,6 @@ import '../../models/themes/quill_icon_theme.dart'; import '../controller.dart'; import '../toolbar.dart'; import 'image_video_utils.dart'; -import 'quill_icon_button.dart'; class CameraButton extends StatelessWidget { const CameraButton({ diff --git a/lib/src/widgets/toolbar/clear_format_button.dart b/lib/src/widgets/toolbar/clear_format_button.dart index 7410c637..652b783f 100644 --- a/lib/src/widgets/toolbar/clear_format_button.dart +++ b/lib/src/widgets/toolbar/clear_format_button.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import '../../../flutter_quill.dart'; -import 'quill_icon_button.dart'; class ClearFormatButton extends StatefulWidget { const ClearFormatButton({ diff --git a/lib/src/widgets/toolbar/color_button.dart b/lib/src/widgets/toolbar/color_button.dart index c0d323bf..d6c90731 100644 --- a/lib/src/widgets/toolbar/color_button.dart +++ b/lib/src/widgets/toolbar/color_button.dart @@ -8,7 +8,6 @@ import '../../translations/toolbar.i18n.dart'; import '../../utils/color.dart'; import '../controller.dart'; import '../toolbar.dart'; -import 'quill_icon_button.dart'; /// Controls color styles. /// diff --git a/lib/src/widgets/toolbar/history_button.dart b/lib/src/widgets/toolbar/history_button.dart index f9a8bf93..a245c2e8 100644 --- a/lib/src/widgets/toolbar/history_button.dart +++ b/lib/src/widgets/toolbar/history_button.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import '../../../flutter_quill.dart'; -import 'quill_icon_button.dart'; class HistoryButton extends StatefulWidget { const HistoryButton({ diff --git a/lib/src/widgets/toolbar/image_button.dart b/lib/src/widgets/toolbar/image_button.dart index 09616e7c..2d42c192 100644 --- a/lib/src/widgets/toolbar/image_button.dart +++ b/lib/src/widgets/toolbar/image_button.dart @@ -4,12 +4,10 @@ import 'package:image_picker/image_picker.dart'; import '../../models/documents/nodes/embed.dart'; import '../../models/themes/quill_dialog_theme.dart'; import '../../models/themes/quill_icon_theme.dart'; -import '../../utils/media_pick_setting.dart'; import '../controller.dart'; import '../link_dialog.dart'; import '../toolbar.dart'; import 'image_video_utils.dart'; -import 'quill_icon_button.dart'; class ImageButton extends StatelessWidget { const ImageButton({ diff --git a/lib/src/widgets/toolbar/image_video_utils.dart b/lib/src/widgets/toolbar/image_video_utils.dart index c4591dd4..f2814ec9 100644 --- a/lib/src/widgets/toolbar/image_video_utils.dart +++ b/lib/src/widgets/toolbar/image_video_utils.dart @@ -6,7 +6,6 @@ import 'package:image_picker/image_picker.dart'; import '../../models/documents/nodes/embed.dart'; import '../../translations/toolbar.i18n.dart'; -import '../../utils/media_pick_setting.dart'; import '../controller.dart'; import '../toolbar.dart'; diff --git a/lib/src/widgets/toolbar/indent_button.dart b/lib/src/widgets/toolbar/indent_button.dart index d72718fc..476ebc46 100644 --- a/lib/src/widgets/toolbar/indent_button.dart +++ b/lib/src/widgets/toolbar/indent_button.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import '../../../flutter_quill.dart'; -import 'quill_icon_button.dart'; class IndentButton extends StatefulWidget { const IndentButton({ diff --git a/lib/src/widgets/toolbar/insert_embed_button.dart b/lib/src/widgets/toolbar/insert_embed_button.dart index 91a6822c..45a4d5ba 100644 --- a/lib/src/widgets/toolbar/insert_embed_button.dart +++ b/lib/src/widgets/toolbar/insert_embed_button.dart @@ -4,7 +4,6 @@ import '../../models/documents/nodes/embed.dart'; import '../../models/themes/quill_icon_theme.dart'; import '../controller.dart'; import '../toolbar.dart'; -import 'quill_icon_button.dart'; class InsertEmbedButton extends StatelessWidget { const InsertEmbedButton({ diff --git a/lib/src/widgets/toolbar/link_style_button.dart b/lib/src/widgets/toolbar/link_style_button.dart index 5ec66b29..35fca8a6 100644 --- a/lib/src/widgets/toolbar/link_style_button.dart +++ b/lib/src/widgets/toolbar/link_style_button.dart @@ -7,7 +7,6 @@ import '../../translations/toolbar.i18n.dart'; import '../controller.dart'; import '../link_dialog.dart'; import '../toolbar.dart'; -import 'quill_icon_button.dart'; class LinkStyleButton extends StatefulWidget { const LinkStyleButton({ diff --git a/lib/src/widgets/toolbar/select_alignment_button.dart b/lib/src/widgets/toolbar/select_alignment_button.dart index 98eb90ad..5ded1021 100644 --- a/lib/src/widgets/toolbar/select_alignment_button.dart +++ b/lib/src/widgets/toolbar/select_alignment_button.dart @@ -84,7 +84,7 @@ class _SelectAlignmentButtonState extends State { mainAxisSize: MainAxisSize.min, children: List.generate(buttonCount, (index) { return Padding( - padding: const EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0), + padding: EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0), child: ConstrainedBox( constraints: BoxConstraints.tightFor( width: widget.iconSize * kIconButtonFactor, diff --git a/lib/src/widgets/toolbar/select_header_style_button.dart b/lib/src/widgets/toolbar/select_header_style_button.dart index bb6eee65..3244b31d 100644 --- a/lib/src/widgets/toolbar/select_header_style_button.dart +++ b/lib/src/widgets/toolbar/select_header_style_button.dart @@ -67,7 +67,7 @@ class _SelectHeaderStyleButtonState extends State { mainAxisSize: MainAxisSize.min, children: List.generate(4, (index) { return Padding( - padding: const EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0), + padding: EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0), child: ConstrainedBox( constraints: BoxConstraints.tightFor( width: widget.iconSize * kIconButtonFactor, diff --git a/lib/src/widgets/toolbar/toggle_check_list_button.dart b/lib/src/widgets/toolbar/toggle_check_list_button.dart index e631d23a..ed24a84d 100644 --- a/lib/src/widgets/toolbar/toggle_check_list_button.dart +++ b/lib/src/widgets/toolbar/toggle_check_list_button.dart @@ -5,7 +5,6 @@ import '../../models/documents/style.dart'; import '../../models/themes/quill_icon_theme.dart'; import '../controller.dart'; import '../toolbar.dart'; -import 'toggle_style_button.dart'; class ToggleCheckListButton extends StatefulWidget { const ToggleCheckListButton({ diff --git a/lib/src/widgets/toolbar/toggle_style_button.dart b/lib/src/widgets/toolbar/toggle_style_button.dart index caf36879..e4a680fe 100644 --- a/lib/src/widgets/toolbar/toggle_style_button.dart +++ b/lib/src/widgets/toolbar/toggle_style_button.dart @@ -5,7 +5,6 @@ import '../../models/documents/style.dart'; import '../../models/themes/quill_icon_theme.dart'; import '../controller.dart'; import '../toolbar.dart'; -import 'quill_icon_button.dart'; typedef ToggleStyleButtonBuilder = Widget Function( BuildContext context, diff --git a/lib/src/widgets/toolbar/video_button.dart b/lib/src/widgets/toolbar/video_button.dart index db2afdeb..dbda51d3 100644 --- a/lib/src/widgets/toolbar/video_button.dart +++ b/lib/src/widgets/toolbar/video_button.dart @@ -4,12 +4,10 @@ import 'package:image_picker/image_picker.dart'; import '../../models/documents/nodes/embed.dart'; import '../../models/themes/quill_dialog_theme.dart'; import '../../models/themes/quill_icon_theme.dart'; -import '../../utils/media_pick_setting.dart'; import '../controller.dart'; import '../link_dialog.dart'; import '../toolbar.dart'; import 'image_video_utils.dart'; -import 'quill_icon_button.dart'; class VideoButton extends StatelessWidget { const VideoButton({ From 890e5842e57076784e9fe7af2ae8a128c89211af Mon Sep 17 00:00:00 2001 From: X Code Date: Thu, 9 Dec 2021 13:32:46 -0800 Subject: [PATCH 106/179] Upgrade to 2.2.0: Support flutter 2.8 --- CHANGELOG.md | 3 +++ lib/src/widgets/raw_editor.dart | 1 + .../raw_editor/raw_editor_state_keyboard_mixin.dart | 12 +++++++----- .../raw_editor_state_selection_delegate_mixin.dart | 5 +++-- pubspec.yaml | 2 +- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 006f50f6..050b375f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.2.0] +* Support flutter 2.8. + ## [2.1.1] * Add methods of clearing editor and moving cursor. diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 1a4f3b6a..f42deef2 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -725,6 +725,7 @@ class RawEditorState extends EditorState @override bool get wantKeepAlive => widget.focusNode.hasFocus; + @override bool get readOnly => widget.readOnly; } diff --git a/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart index 8156e6ee..05dc0057 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart @@ -100,11 +100,13 @@ mixin RawEditorStateKeyboardMixin on EditorState { TextSelection.collapsed(offset: selection.start), ); - setTextEditingValue(TextEditingValue( - text: - selection.textBefore(plainText) + selection.textAfter(plainText), - selection: TextSelection.collapsed(offset: selection.start), - ), SelectionChangedCause.keyboard); + setTextEditingValue( + TextEditingValue( + text: selection.textBefore(plainText) + + selection.textAfter(plainText), + selection: TextSelection.collapsed(offset: selection.start), + ), + SelectionChangedCause.keyboard); } return; } 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 8f29c2b7..0ee0ed66 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 @@ -131,11 +131,12 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: - // Collapse the selection and hide the toolbar and handles. + // Collapse the selection and hide the toolbar and handles. userUpdateTextEditingValue( TextEditingValue( text: textEditingValue.text, - selection: TextSelection.collapsed(offset: textEditingValue.selection.end), + selection: TextSelection.collapsed( + offset: textEditingValue.selection.end), ), SelectionChangedCause.toolbar, ); diff --git a/pubspec.yaml b/pubspec.yaml index 21febb2a..ae629332 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: 2.1.1 +version: 2.2.0 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 7d5befd7e3893fe1dfe1b9468a51c02ddcdcd483 Mon Sep 17 00:00:00 2001 From: Andy Trand Date: Fri, 10 Dec 2021 01:43:46 +0200 Subject: [PATCH 107/179] flutter version, import fixes (#517) --- lib/src/widgets/style_widgets/number_point.dart | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/widgets/style_widgets/number_point.dart b/lib/src/widgets/style_widgets/number_point.dart index 16dd1e4f..8fec668c 100644 --- a/lib/src/widgets/style_widgets/number_point.dart +++ b/lib/src/widgets/style_widgets/number_point.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '/models/documents/attribute.dart'; -import '/widgets/text_block.dart'; +import '../../models/documents/attribute.dart'; +import '../text_block.dart'; class QuillNumberPoint extends StatelessWidget { const QuillNumberPoint({ diff --git a/pubspec.yaml b/pubspec.yaml index ae629332..bf62ba39 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ repository: https://github.com/singerdmx/flutter-quill environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.17.0" + flutter: ">=2.8.0" dependencies: flutter: From 4c6390bfa5fdec1dd4451db67d4d491668dc9b7c Mon Sep 17 00:00:00 2001 From: X Code Date: Thu, 9 Dec 2021 15:46:10 -0800 Subject: [PATCH 108/179] Upgrade to 2.2.1 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 050b375f..25f01f1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.2.1] +* Bug fix for imports supporting flutter 2.8. + ## [2.2.0] * Support flutter 2.8. diff --git a/pubspec.yaml b/pubspec.yaml index bf62ba39..d770ba66 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: 2.2.0 +version: 2.2.1 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 2b8fc44a5d45b11692668a44a586ea0e609a4438 Mon Sep 17 00:00:00 2001 From: X Code Date: Thu, 9 Dec 2021 21:21:50 -0800 Subject: [PATCH 109/179] Make CursorPainter constructor named --- lib/src/widgets/cursor.dart | 14 +++++++------- lib/src/widgets/text_line.dart | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/src/widgets/cursor.dart b/lib/src/widgets/cursor.dart index dd449c7c..0fe57355 100644 --- a/lib/src/widgets/cursor.dart +++ b/lib/src/widgets/cursor.dart @@ -228,13 +228,13 @@ class CursorCont extends ChangeNotifier { /// Paints the editing cursor. class CursorPainter { - CursorPainter( - this.editable, - this.style, - this.prototype, - this.color, - this.devicePixelRatio, - ); + CursorPainter({ + required this.editable, + required this.style, + required this.prototype, + required this.color, + required this.devicePixelRatio, + }); final RenderContentProxyBox? editable; final CursorStyle style; diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index 14546d11..2291dfc3 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -815,11 +815,11 @@ class RenderEditableTextLine extends RenderEditableBox { } CursorPainter get _cursorPainter => CursorPainter( - _body, - cursorCont.style, - _caretPrototype!, - cursorCont.color.value, - devicePixelRatio, + editable: _body, + style: cursorCont.style, + prototype: _caretPrototype!, + color: cursorCont.color.value, + devicePixelRatio: devicePixelRatio, ); @override From 13d542680e538f7efa0d88b152ef4b2b96f597a3 Mon Sep 17 00:00:00 2001 From: X Code Date: Fri, 10 Dec 2021 00:34:18 -0800 Subject: [PATCH 110/179] iOS - floating cursor --- lib/src/widgets/box.dart | 4 + lib/src/widgets/cursor.dart | 11 + lib/src/widgets/editor.dart | 196 ++++++++++++++++-- lib/src/widgets/float_cursor.dart | 31 +++ lib/src/widgets/raw_editor.dart | 43 ++-- ..._editor_state_text_input_client_mixin.dart | 115 +++++++++- lib/src/widgets/simple_viewer.dart | 48 +++-- lib/src/widgets/text_block.dart | 10 + lib/src/widgets/text_line.dart | 55 ++++- 9 files changed, 454 insertions(+), 59 deletions(-) create mode 100644 lib/src/widgets/float_cursor.dart diff --git a/lib/src/widgets/box.dart b/lib/src/widgets/box.dart index 566c1a92..b754b5c2 100644 --- a/lib/src/widgets/box.dart +++ b/lib/src/widgets/box.dart @@ -119,4 +119,8 @@ abstract class RenderEditableBox extends RenderBox { /// Returns the [Rect] in local coordinates for the caret at the given text /// position. Rect getLocalRectForCaret(TextPosition position); + + /// Returns the [Rect] of the caret prototype at the given text + /// position. [Rect] starts at origin. + Rect getCaretPrototype(TextPosition position); } diff --git a/lib/src/widgets/cursor.dart b/lib/src/widgets/cursor.dart index 0fe57355..92666d82 100644 --- a/lib/src/widgets/cursor.dart +++ b/lib/src/widgets/cursor.dart @@ -131,6 +131,17 @@ class CursorCont extends ChangeNotifier { Timer? _cursorTimer; bool _targetCursorVisibility = false; + final ValueNotifier _floatingCursorTextPosition = + ValueNotifier(null); + + ValueNotifier get floatingCursorTextPosition => + _floatingCursorTextPosition; + + void setFloatingCursorTextPosition(TextPosition? position) => + _floatingCursorTextPosition.value = position; + + bool get isFloatingCursorActive => floatingCursorTextPosition.value != null; + CursorStyle _style; CursorStyle get style => _style; set style(CursorStyle value) { diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index c12c314d..17ef54a8 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.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:url_launcher/url_launcher.dart'; @@ -22,6 +23,7 @@ import 'controller.dart'; import 'cursor.dart'; import 'default_styles.dart'; import 'delegate.dart'; +import 'float_cursor.dart'; import 'image.dart'; import 'raw_editor.dart'; import 'text_selection.dart'; @@ -56,6 +58,10 @@ abstract class EditorState extends State { EditorTextSelectionOverlay? getSelectionOverlay(); + /// Controls the floating cursor animation when it is released. + /// The floating cursor is animated to merge with the regular cursor. + AnimationController get floatingCursorResetController; + bool showToolbar(); void hideToolbar(); @@ -88,9 +94,24 @@ abstract class RenderAbstractEditor { /// selection that contains some text but whose ends meet in the middle). TextPosition getPositionForOffset(Offset offset); + /// Returns the local coordinates of the endpoints of the given selection. + /// + /// If the selection is collapsed (and therefore occupies a single point), the + /// returned list is of length one. Otherwise, the selection is not collapsed + /// and the returned list is of length two. In this case, however, the two + /// points might actually be co-located (e.g., because of a bidirectional + /// selection that contains some text but whose ends meet in the middle). List getEndpointsForSelection( TextSelection textSelection); + /// Sets the screen position of the floating cursor and the text position + /// closest to the cursor. + /// `resetLerpValue` drives the size of the floating cursor. + /// See [EditorState.floatingCursorResetController]. + void setFloatingCursor(FloatingCursorDragState dragState, + Offset lastBoundedOffset, TextPosition lastTextPosition, + {double? resetLerpValue}); + /// If [ignorePointer] is false (the default) then this method is called by /// the internal gesture recognizer's [TapGestureRecognizer.onTapDown] /// callback. @@ -646,25 +667,39 @@ class _QuillEditorSelectionGestureDetectorBuilder } } +/// Signature for the callback that reports when the user changes the selection +/// (including the cursor location). +/// +/// Used by [RenderEditor.onSelectionChanged]. typedef TextSelectionChangedHandler = void Function( TextSelection selection, SelectionChangedCause cause); +// The padding applied to text field. Used to determine the bounds when +// moving the floating cursor. +const EdgeInsets _kFloatingCursorAddedMargin = EdgeInsets.fromLTRB(4, 4, 4, 5); + +// The additional size on the x and y axis with which to expand the prototype +// cursor to render the floating cursor in pixels. +const EdgeInsets _kFloatingCaretSizeIncrease = + EdgeInsets.symmetric(horizontal: 0.5, vertical: 1); + class RenderEditor extends RenderEditableContainerBox implements RenderAbstractEditor { RenderEditor( - ViewportOffset? offset, - List? children, - TextDirection textDirection, - double scrollBottomInset, - EdgeInsetsGeometry padding, - this.document, - this.selection, - this._hasFocus, - this.onSelectionChanged, - this._startHandleLayerLink, - this._endHandleLayerLink, - EdgeInsets floatingCursorAddedMargin, - ) : super( + ViewportOffset? offset, + List? children, + TextDirection textDirection, + double scrollBottomInset, + EdgeInsetsGeometry padding, + this.document, + this.selection, + this._hasFocus, + this.onSelectionChanged, + this._startHandleLayerLink, + this._endHandleLayerLink, + EdgeInsets floatingCursorAddedMargin, + this._cursorController) + : super( children, document.root, textDirection, @@ -672,6 +707,8 @@ class RenderEditor extends RenderEditableContainerBox padding, ); + final CursorCont _cursorController; + Document document; TextSelection selection; bool _hasFocus = false; @@ -983,9 +1020,20 @@ class RenderEditor extends RenderEditableContainerBox @override void paint(PaintingContext context, Offset offset) { + if (_hasFocus && + _cursorController.show.value && + !_cursorController.style.paintAboveText) { + _paintFloatingCursor(context, offset); + } defaultPaint(context, offset); _updateSelectionExtentsVisibility(offset + _paintOffset); _paintHandleLayers(context, getEndpointsForSelection(selection)); + + if (_hasFocus && + _cursorController.show.value && + _cursorController.style.paintAboveText) { + _paintFloatingCursor(context, offset); + } } @override @@ -1097,6 +1145,128 @@ class RenderEditor extends RenderEditableContainerBox final boxParentData = targetChild.parentData as BoxParentData; return childLocalRect.shift(Offset(0, boxParentData.offset.dy)); } + + // Start floating cursor + + FloatingCursorPainter get _floatingCursorPainter => FloatingCursorPainter( + floatingCursorRect: _floatingCursorRect, + style: _cursorController.style, + ); + + bool _floatingCursorOn = false; + Rect? _floatingCursorRect; + + TextPosition get floatingCursorTextPosition => _floatingCursorTextPosition; + late TextPosition _floatingCursorTextPosition; + + // The relative origin in relation to the distance the user has theoretically + // dragged the floating cursor offscreen. + // This value is used to account for the difference + // in the rendering position and the raw offset value. + Offset _relativeOrigin = Offset.zero; + Offset? _previousOffset; + bool _resetOriginOnLeft = false; + bool _resetOriginOnRight = false; + bool _resetOriginOnTop = false; + bool _resetOriginOnBottom = false; + + /// Returns the position within the editor closest to the raw cursor offset. + Offset calculateBoundedFloatingCursorOffset( + Offset rawCursorOffset, double preferredLineHeight) { + var deltaPosition = Offset.zero; + final topBound = _kFloatingCursorAddedMargin.top; + final bottomBound = + size.height - preferredLineHeight + _kFloatingCursorAddedMargin.bottom; + final leftBound = _kFloatingCursorAddedMargin.left; + final rightBound = size.width - _kFloatingCursorAddedMargin.right; + + if (_previousOffset != null) { + deltaPosition = rawCursorOffset - _previousOffset!; + } + + // If the raw cursor offset has gone off an edge, + // we want to reset the relative origin of + // the dragging when the user drags back into the field. + if (_resetOriginOnLeft && deltaPosition.dx > 0) { + _relativeOrigin = + Offset(rawCursorOffset.dx - leftBound, _relativeOrigin.dy); + _resetOriginOnLeft = false; + } else if (_resetOriginOnRight && deltaPosition.dx < 0) { + _relativeOrigin = + Offset(rawCursorOffset.dx - rightBound, _relativeOrigin.dy); + _resetOriginOnRight = false; + } + if (_resetOriginOnTop && deltaPosition.dy > 0) { + _relativeOrigin = + Offset(_relativeOrigin.dx, rawCursorOffset.dy - topBound); + _resetOriginOnTop = false; + } else if (_resetOriginOnBottom && deltaPosition.dy < 0) { + _relativeOrigin = + Offset(_relativeOrigin.dx, rawCursorOffset.dy - bottomBound); + _resetOriginOnBottom = false; + } + + final currentX = rawCursorOffset.dx - _relativeOrigin.dx; + final currentY = rawCursorOffset.dy - _relativeOrigin.dy; + final double adjustedX = + math.min(math.max(currentX, leftBound), rightBound); + final double adjustedY = + math.min(math.max(currentY, topBound), bottomBound); + final adjustedOffset = Offset(adjustedX, adjustedY); + + if (currentX < leftBound && deltaPosition.dx < 0) { + _resetOriginOnLeft = true; + } else if (currentX > rightBound && deltaPosition.dx > 0) { + _resetOriginOnRight = true; + } + if (currentY < topBound && deltaPosition.dy < 0) { + _resetOriginOnTop = true; + } else if (currentY > bottomBound && deltaPosition.dy > 0) { + _resetOriginOnBottom = true; + } + + _previousOffset = rawCursorOffset; + + return adjustedOffset; + } + + @override + void setFloatingCursor(FloatingCursorDragState dragState, + Offset boundedOffset, TextPosition textPosition, + {double? resetLerpValue}) { + if (dragState == FloatingCursorDragState.Start) { + _relativeOrigin = Offset.zero; + _previousOffset = null; + _resetOriginOnBottom = false; + _resetOriginOnTop = false; + _resetOriginOnRight = false; + _resetOriginOnBottom = false; + } + _floatingCursorOn = dragState != FloatingCursorDragState.End; + if (_floatingCursorOn) { + _floatingCursorTextPosition = textPosition; + final sizeAdjustment = resetLerpValue != null + ? EdgeInsets.lerp( + _kFloatingCaretSizeIncrease, EdgeInsets.zero, resetLerpValue)! + : _kFloatingCaretSizeIncrease; + final child = childAtPosition(textPosition); + final caretPrototype = + child.getCaretPrototype(child.globalToLocalPosition(textPosition)); + _floatingCursorRect = + sizeAdjustment.inflateRect(caretPrototype).shift(boundedOffset); + _cursorController + .setFloatingCursorTextPosition(_floatingCursorTextPosition); + } else { + _floatingCursorRect = null; + _cursorController.setFloatingCursorTextPosition(null); + } + } + + void _paintFloatingCursor(PaintingContext context, Offset offset) { + _floatingCursorPainter.paint(context.canvas); + } + +// End floating cursor } class EditableContainerParentData diff --git a/lib/src/widgets/float_cursor.dart b/lib/src/widgets/float_cursor.dart new file mode 100644 index 00000000..b67efc29 --- /dev/null +++ b/lib/src/widgets/float_cursor.dart @@ -0,0 +1,31 @@ +// The corner radius of the floating cursor in pixels. +import 'dart:ui'; + +import '../../widgets/cursor.dart'; + +const Radius _kFloatingCaretRadius = Radius.circular(1); + +/// Floating painter responsible for painting the floating cursor when +/// floating mode is activated +class FloatingCursorPainter { + FloatingCursorPainter({ + required this.floatingCursorRect, + required this.style, + }); + + CursorStyle style; + + Rect? floatingCursorRect; + + final Paint floatingCursorPaint = Paint(); + + void paint(Canvas canvas) { + final floatingCursorRect = this.floatingCursorRect; + final floatingCursorColor = style.color.withOpacity(0.75); + if (floatingCursorRect == null) return; + canvas.drawRRect( + RRect.fromRectAndRadius(floatingCursorRect, _kFloatingCaretRadius), + floatingCursorPaint..color = floatingCursorColor, + ); + } +} diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index f42deef2..eef259f6 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -126,6 +126,7 @@ class RawEditorState extends EditorState ScrollController get scrollController => _scrollController; late ScrollController _scrollController; + // Cursors late CursorCont _cursorCont; // Focus @@ -133,6 +134,7 @@ class RawEditorState extends EditorState FocusAttachment? _focusAttachment; bool get _hasFocus => widget.focusNode.hasFocus; + // Theme DefaultStyles? _styles; final ClipboardStatusNotifier _clipboardStatus = ClipboardStatusNotifier(); @@ -162,6 +164,7 @@ class RawEditorState extends EditorState document: _doc, selection: widget.controller.selection, hasFocus: _hasFocus, + cursorController: _cursorCont, textDirection: _textDirection, startHandleLayerLink: _startHandleLayerLink, endHandleLayerLink: _endHandleLayerLink, @@ -196,6 +199,7 @@ class RawEditorState extends EditorState onSelectionChanged: _handleSelectionChanged, scrollBottomInset: widget.scrollBottomInset, padding: widget.padding, + cursorController: _cursorCont, children: _buildChildren(_doc, context), ), ), @@ -362,6 +366,11 @@ class RawEditorState extends EditorState tickerProvider: this, ); + // Floating cursor + _floatingCursorResetController = AnimationController(vsync: this); + _floatingCursorResetController.addListener(onFloatingCursorResetTick); + + // Keyboard _keyboardListener = KeyboardEventHandler( handleCursorMovement, handleShortcut, @@ -727,6 +736,12 @@ class RawEditorState extends EditorState @override bool get readOnly => widget.readOnly; + + @override + AnimationController get floatingCursorResetController => + _floatingCursorResetController; + + late AnimationController _floatingCursorResetController; } class _Editor extends MultiChildRenderObjectWidget { @@ -741,6 +756,7 @@ class _Editor extends MultiChildRenderObjectWidget { required this.endHandleLayerLink, required this.onSelectionChanged, required this.scrollBottomInset, + required this.cursorController, this.padding = EdgeInsets.zero, this.offset, }) : super(key: key, children: children); @@ -755,23 +771,24 @@ class _Editor extends MultiChildRenderObjectWidget { final TextSelectionChangedHandler onSelectionChanged; final double scrollBottomInset; final EdgeInsetsGeometry padding; + final CursorCont cursorController; @override RenderEditor createRenderObject(BuildContext context) { return RenderEditor( - offset, - null, - textDirection, - scrollBottomInset, - padding, - document, - selection, - hasFocus, - onSelectionChanged, - startHandleLayerLink, - endHandleLayerLink, - const EdgeInsets.fromLTRB(4, 4, 4, 5), - ); + offset, + null, + textDirection, + scrollBottomInset, + padding, + document, + selection, + hasFocus, + onSelectionChanged, + startHandleLayerLink, + endHandleLayerLink, + const EdgeInsets.fromLTRB(4, 4, 4, 5), + cursorController); } @override diff --git a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart index 37c5ece1..cfb13e62 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart @@ -1,3 +1,6 @@ +import 'dart:ui'; + +import 'package:flutter/animation.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -185,9 +188,119 @@ mixin RawEditorStateTextInputClientMixin on EditorState // no-op } + // The time it takes for the floating cursor to snap to the text aligned + // cursor position after the user has finished placing it. + static const Duration _floatingCursorResetTime = Duration(milliseconds: 125); + + // The original position of the caret on FloatingCursorDragState.start. + Rect? _startCaretRect; + + // The most recent text position as determined by the location of the floating + // cursor. + TextPosition? _lastTextPosition; + + // The offset of the floating cursor as determined from the start call. + Offset? _pointOffsetOrigin; + + // The most recent position of the floating cursor. + Offset? _lastBoundedOffset; + + // Because the center of the cursor is preferredLineHeight / 2 below the touch + // origin, but the touch origin is used to determine which line the cursor is + // on, we need this offset to correctly render and move the cursor. + Offset _floatingCursorOffset(TextPosition textPosition) => + Offset(0, getRenderEditor()!.preferredLineHeight(textPosition) / 2); + @override void updateFloatingCursor(RawFloatingCursorPoint point) { - throw UnimplementedError(); + switch (point.state) { + case FloatingCursorDragState.Start: + if (floatingCursorResetController.isAnimating) { + floatingCursorResetController.stop(); + onFloatingCursorResetTick(); + } + // We want to send in points that are centered around a (0,0) origin, so + // we cache the position. + _pointOffsetOrigin = point.offset; + + final currentTextPosition = + TextPosition(offset: getRenderEditor()!.selection.baseOffset); + _startCaretRect = + getRenderEditor()!.getLocalRectForCaret(currentTextPosition); + + _lastBoundedOffset = _startCaretRect!.center - + _floatingCursorOffset(currentTextPosition); + _lastTextPosition = currentTextPosition; + getRenderEditor()!.setFloatingCursor( + point.state, _lastBoundedOffset!, _lastTextPosition!); + break; + case FloatingCursorDragState.Update: + assert(_lastTextPosition != null, 'Last text position was not set'); + final floatingCursorOffset = _floatingCursorOffset(_lastTextPosition!); + final centeredPoint = point.offset! - _pointOffsetOrigin!; + final rawCursorOffset = + _startCaretRect!.center + centeredPoint - floatingCursorOffset; + + final preferredLineHeight = + getRenderEditor()!.preferredLineHeight(_lastTextPosition!); + _lastBoundedOffset = + getRenderEditor()!.calculateBoundedFloatingCursorOffset( + rawCursorOffset, + preferredLineHeight, + ); + _lastTextPosition = getRenderEditor()!.getPositionForOffset( + getRenderEditor()! + .localToGlobal(_lastBoundedOffset! + floatingCursorOffset)); + getRenderEditor()!.setFloatingCursor( + point.state, _lastBoundedOffset!, _lastTextPosition!); + final newSelection = TextSelection.collapsed( + offset: _lastTextPosition!.offset, + affinity: _lastTextPosition!.affinity); + // Setting selection as floating cursor moves will have scroll view + // bring background cursor into view + getRenderEditor()! + .onSelectionChanged(newSelection, SelectionChangedCause.forcePress); + break; + case FloatingCursorDragState.End: + // We skip animation if no update has happened. + if (_lastTextPosition != null && _lastBoundedOffset != null) { + floatingCursorResetController + ..value = 0.0 + ..animateTo(1, + duration: _floatingCursorResetTime, curve: Curves.decelerate); + } + break; + } + } + + /// Specifies the floating cursor dimensions and position based + /// the animation controller value. + /// The floating cursor is resized + /// (see [RenderAbstractEditor.setFloatingCursor]) + /// and repositioned (linear interpolation between position of floating cursor + /// and current position of background cursor) + void onFloatingCursorResetTick() { + final finalPosition = + getRenderEditor()!.getLocalRectForCaret(_lastTextPosition!).centerLeft - + _floatingCursorOffset(_lastTextPosition!); + if (floatingCursorResetController.isCompleted) { + getRenderEditor()!.setFloatingCursor( + FloatingCursorDragState.End, finalPosition, _lastTextPosition!); + _startCaretRect = null; + _lastTextPosition = null; + _pointOffsetOrigin = null; + _lastBoundedOffset = null; + } else { + final lerpValue = floatingCursorResetController.value; + final lerpX = + lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!; + final lerpY = + lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!; + + getRenderEditor()!.setFloatingCursor(FloatingCursorDragState.Update, + Offset(lerpX, lerpY), _lastTextPosition!, + resetLerpValue: lerpValue); + } } @override diff --git a/lib/src/widgets/simple_viewer.dart b/lib/src/widgets/simple_viewer.dart index 86dcd4f3..9a2dc221 100644 --- a/lib/src/widgets/simple_viewer.dart +++ b/lib/src/widgets/simple_viewer.dart @@ -150,15 +150,15 @@ class _QuillSimpleViewerState extends State 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), - ), + document: _doc, + textDirection: _textDirection, + startHandleLayerLink: _startHandleLayerLink, + endHandleLayerLink: _endHandleLayerLink, + onSelectionChanged: _nullSelectionChanged, + scrollBottomInset: widget.scrollBottomInset, + padding: widget.padding, + cursorController: _cursorCont, + children: _buildChildren(_doc, context)), ), ); @@ -315,6 +315,7 @@ class _SimpleViewer extends MultiChildRenderObjectWidget { required this.endHandleLayerLink, required this.onSelectionChanged, required this.scrollBottomInset, + required this.cursorController, this.offset, this.padding = EdgeInsets.zero, Key? key, @@ -328,24 +329,25 @@ class _SimpleViewer extends MultiChildRenderObjectWidget { final TextSelectionChangedHandler onSelectionChanged; final double scrollBottomInset; final EdgeInsetsGeometry padding; + final CursorCont cursorController; @override RenderEditor createRenderObject(BuildContext context) { return RenderEditor( - offset, - null, - textDirection, - scrollBottomInset, - padding, - document, - const TextSelection(baseOffset: 0, extentOffset: 0), - false, - // hasFocus, - onSelectionChanged, - startHandleLayerLink, - endHandleLayerLink, - const EdgeInsets.fromLTRB(4, 4, 4, 5), - ); + offset, + null, + textDirection, + scrollBottomInset, + padding, + document, + const TextSelection(baseOffset: 0, extentOffset: 0), + false, + // hasFocus, + onSelectionChanged, + startHandleLayerLink, + endHandleLayerLink, + const EdgeInsets.fromLTRB(4, 4, 4, 5), + cursorController); } @override diff --git a/lib/src/widgets/text_block.dart b/lib/src/widgets/text_block.dart index 245cd332..33b1c351 100644 --- a/lib/src/widgets/text_block.dart +++ b/lib/src/widgets/text_block.dart @@ -560,6 +560,16 @@ class RenderEditableTextBlock extends RenderEditableContainerBox affinity: position.affinity, ); } + + @override + Rect getCaretPrototype(TextPosition position) { + final child = childAtPosition(position); + final localPosition = TextPosition( + offset: position.offset - child.getContainer().offset, + affinity: position.affinity, + ); + return child.getCaretPrototype(localPosition); + } } class _EditableBlock extends MultiChildRenderObjectWidget { diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index 2291dfc3..5e365272 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -387,7 +387,7 @@ class RenderEditableTextLine extends RenderEditableBox { EdgeInsets? _resolvedPadding; bool? _containsCursor; List? _selectedRects; - Rect? _caretPrototype; + late Rect _caretPrototype; final Map children = {}; Iterable get _children sync* { @@ -501,8 +501,11 @@ class RenderEditableTextLine extends RenderEditableBox { } bool containsCursor() { - return _containsCursor ??= textSelection.isCollapsed && - line.containsOffset(textSelection.baseOffset); + return _containsCursor ??= cursorCont.isFloatingCursorActive + ? line + .containsOffset(cursorCont.floatingCursorTextPosition.value!.offset) + : textSelection.isCollapsed && + line.containsOffset(textSelection.baseOffset); } RenderBox? _updateChild( @@ -638,6 +641,17 @@ class RenderEditableTextLine extends RenderEditableBox { cursorCont.style.height ?? preferredLineHeight(const TextPosition(offset: 0)); + // TODO: This is no longer producing the highest-fidelity caret + // heights for Android, especially when non-alphabetic languages + // are involved. The current implementation overrides the height set + // here with the full measured height of the text on Android which looks + // superior (subjectively and in terms of fidelity) in _paintCaret. We + // should rework this properly to once again match the platform. The constant + // _kCaretHeightOffset scales poorly for small font sizes. + // + /// On iOS, the cursor is taller than the cursor on Android. The height + /// of the cursor for iOS is approximate and obtained through an eyeball + /// comparison. void _computeCaretPrototype() { switch (defaultTargetPlatform) { case TargetPlatform.iOS: @@ -655,12 +669,24 @@ class RenderEditableTextLine extends RenderEditableBox { } } + void _onFloatingCursorChange() { + _containsCursor = null; + markNeedsPaint(); + } + + // End caret implementation + + // + + // Start render box overrides + @override void attach(covariant PipelineOwner owner) { super.attach(owner); for (final child in _children) { child.attach(owner); } + cursorCont.floatingCursorTextPosition.addListener(_onFloatingCursorChange); if (containsCursor()) { cursorCont.addListener(markNeedsLayout); cursorCont.color.addListener(safeMarkNeedsPaint); @@ -673,6 +699,8 @@ class RenderEditableTextLine extends RenderEditableBox { for (final child in _children) { child.detach(); } + cursorCont.floatingCursorTextPosition + .removeListener(_onFloatingCursorChange); if (containsCursor()) { cursorCont.removeListener(markNeedsLayout); cursorCont.color.removeListener(safeMarkNeedsPaint); @@ -817,8 +845,10 @@ class RenderEditableTextLine extends RenderEditableBox { CursorPainter get _cursorPainter => CursorPainter( editable: _body, style: cursorCont.style, - prototype: _caretPrototype!, - color: cursorCont.color.value, + prototype: _caretPrototype, + color: cursorCont.isFloatingCursorActive + ? cursorCont.style.backgroundColor + : cursorCont.color.value, devicePixelRatio: devicePixelRatio, ); @@ -873,10 +903,14 @@ class RenderEditableTextLine extends RenderEditableBox { void _paintCursor( PaintingContext context, Offset effectiveOffset, bool lineHasEmbed) { - final position = TextPosition( - offset: textSelection.extentOffset - line.documentOffset, - affinity: textSelection.base.affinity, - ); + final position = cursorCont.isFloatingCursorActive + ? TextPosition( + offset: cursorCont.floatingCursorTextPosition.value!.offset - + line.documentOffset, + affinity: cursorCont.floatingCursorTextPosition.value!.affinity) + : TextPosition( + offset: textSelection.extentOffset - line.documentOffset, + affinity: textSelection.base.affinity); _cursorPainter.paint( context.canvas, effectiveOffset, position, lineHasEmbed); } @@ -921,6 +955,9 @@ class RenderEditableTextLine extends RenderEditableBox { } markNeedsPaint(); } + + @override + Rect getCaretPrototype(TextPosition position) => _caretPrototype; } class _TextLineElement extends RenderObjectElement { From f530b849b59724c87ca4ecff7ba9823fd5b8df65 Mon Sep 17 00:00:00 2001 From: X Code Date: Fri, 10 Dec 2021 00:44:51 -0800 Subject: [PATCH 111/179] Upgrade to 2.2.2 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25f01f1b..a1ba734c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.2.2] +* iOS - floating cursor. + ## [2.2.1] * Bug fix for imports supporting flutter 2.8. diff --git a/pubspec.yaml b/pubspec.yaml index d770ba66..c4f30c11 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: 2.2.1 +version: 2.2.2 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 49040447b12797127ac3882978329f806b639ab5 Mon Sep 17 00:00:00 2001 From: X Code Date: Fri, 10 Dec 2021 00:45:55 -0800 Subject: [PATCH 112/179] Lower environment flutter version to work with pub.dev --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index c4f30c11..5a5a6bcb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ repository: https://github.com/singerdmx/flutter-quill environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.8.0" + flutter: ">=2.5.3" dependencies: flutter: From 7ee58bf46de258b7bd072e4b80799c96f5fd46cc Mon Sep 17 00:00:00 2001 From: X Code Date: Fri, 10 Dec 2021 07:28:48 -0800 Subject: [PATCH 113/179] Upgrade to 2.8 (#519) --- lib/src/utils/diff_delta.dart | 29 -- lib/src/widgets/editor.dart | 157 ++++++-- lib/src/widgets/keyboard_listener.dart | 129 ------ lib/src/widgets/raw_editor.dart | 105 +++-- .../raw_editor_state_keyboard_mixin.dart | 368 ------------------ ...editor_state_selection_delegate_mixin.dart | 130 +------ ..._editor_state_text_input_client_mixin.dart | 9 +- lib/widgets/keyboard_listener.dart | 3 - .../raw_editor_state_keyboard_mixin.dart | 3 - 9 files changed, 222 insertions(+), 711 deletions(-) delete mode 100644 lib/src/widgets/keyboard_listener.dart delete mode 100644 lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart delete mode 100644 lib/widgets/keyboard_listener.dart delete mode 100644 lib/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart diff --git a/lib/src/utils/diff_delta.dart b/lib/src/utils/diff_delta.dart index f44a1a09..cf0e5c63 100644 --- a/lib/src/utils/diff_delta.dart +++ b/lib/src/utils/diff_delta.dart @@ -2,35 +2,6 @@ import 'dart:math' as math; import '../models/quill_delta.dart'; -const Set WHITE_SPACE = { - 0x9, - 0xA, - 0xB, - 0xC, - 0xD, - 0x1C, - 0x1D, - 0x1E, - 0x1F, - 0x20, - 0xA0, - 0x1680, - 0x2000, - 0x2001, - 0x2002, - 0x2003, - 0x2004, - 0x2005, - 0x2006, - 0x2007, - 0x2008, - 0x2009, - 0x200A, - 0x202F, - 0x205F, - 0x3000 -}; - // Diff between two texts - old text and new text class Diff { Diff(this.start, this.deleted, this.inserted); diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 17ef54a8..94869f8d 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -47,13 +47,10 @@ const linkPrefixes = [ 'http' ]; -abstract class EditorState extends State { +abstract class EditorState extends State + implements TextSelectionDelegate { ScrollController get scrollController; - TextEditingValue getTextEditingValue(); - - void setTextEditingValue(TextEditingValue value, SelectionChangedCause cause); - RenderEditor? getRenderEditor(); EditorTextSelectionOverlay? getSelectionOverlay(); @@ -64,15 +61,11 @@ abstract class EditorState extends State { bool showToolbar(); - void hideToolbar(); - void requestKeyboard(); - - bool get readOnly; } /// Base interface for editable render objects. -abstract class RenderAbstractEditor { +abstract class RenderAbstractEditor implements TextLayoutMetrics { TextSelection selectWordAtPosition(TextPosition position); TextSelection selectLineAtPosition(TextPosition position); @@ -684,6 +677,7 @@ const EdgeInsets _kFloatingCaretSizeIncrease = EdgeInsets.symmetric(horizontal: 0.5, vertical: 1); class RenderEditor extends RenderEditableContainerBox + with RelayoutWhenSystemFontsChangeMixin implements RenderAbstractEditor { RenderEditor( ViewportOffset? offset, @@ -985,15 +979,8 @@ class RenderEditor extends RenderEditableContainerBox @override TextSelection selectWordAtPosition(TextPosition position) { - final child = childAtPosition(position); - final nodeOffset = child.getContainer().offset; - final localPosition = TextPosition( - offset: position.offset - nodeOffset, affinity: position.affinity); - final localWord = child.getWordBoundary(localPosition); - final word = TextRange( - start: localWord.start + nodeOffset, - end: localWord.end + nodeOffset, - ); + final word = getWordBoundary(position); + // When long-pressing past the end of the text, we want a collapsed cursor. if (position.offset >= word.end) { return TextSelection.fromPosition(position); } @@ -1002,16 +989,9 @@ class RenderEditor extends RenderEditableContainerBox @override TextSelection selectLineAtPosition(TextPosition position) { - final child = childAtPosition(position); - final nodeOffset = child.getContainer().offset; - final localPosition = TextPosition( - offset: position.offset - nodeOffset, affinity: position.affinity); - final localLineRange = child.getLineBoundary(localPosition); - final line = TextRange( - start: localLineRange.start + nodeOffset, - end: localLineRange.end + nodeOffset, - ); + final line = getLineAtOffset(position); + // When long-pressing past the end of the text, we want a collapsed cursor. if (position.offset >= line.end) { return TextSelection.fromPosition(position); } @@ -1266,7 +1246,126 @@ class RenderEditor extends RenderEditableContainerBox _floatingCursorPainter.paint(context.canvas); } -// End floating cursor + // End floating cursor + + // Start TextLayoutMetrics implementation + + /// Return a [TextSelection] containing the line of the given [TextPosition]. + @override + TextSelection getLineAtOffset(TextPosition position) { + final child = childAtPosition(position); + final nodeOffset = child.getContainer().offset; + final localPosition = TextPosition( + offset: position.offset - nodeOffset, affinity: position.affinity); + final localLineRange = child.getLineBoundary(localPosition); + final line = TextRange( + start: localLineRange.start + nodeOffset, + end: localLineRange.end + nodeOffset, + ); + return TextSelection(baseOffset: line.start, extentOffset: line.end); + } + + @override + TextRange getWordBoundary(TextPosition position) { + final child = childAtPosition(position); + final nodeOffset = child.getContainer().offset; + final localPosition = TextPosition( + offset: position.offset - nodeOffset, affinity: position.affinity); + final localWord = child.getWordBoundary(localPosition); + return TextRange( + start: localWord.start + nodeOffset, + end: localWord.end + nodeOffset, + ); + } + + /// Returns the TextPosition above the given offset into the text. + /// + /// If the offset is already on the first line, the offset of the first + /// character will be returned. + @override + TextPosition getTextPositionAbove(TextPosition position) { + final child = childAtPosition(position); + final localPosition = TextPosition( + offset: position.offset - child.getContainer().documentOffset); + + var newPosition = child.getPositionAbove(localPosition); + + if (newPosition == null) { + // There was no text above in the current child, check the direct + // sibling. + final sibling = childBefore(child); + if (sibling == null) { + // reached beginning of the document, move to the + // first character + newPosition = const TextPosition(offset: 0); + } else { + final caretOffset = child.getOffsetForCaret(localPosition); + final testPosition = + TextPosition(offset: sibling.getContainer().length - 1); + final testOffset = sibling.getOffsetForCaret(testPosition); + final finalOffset = Offset(caretOffset.dx, testOffset.dy); + final siblingPosition = sibling.getPositionForOffset(finalOffset); + newPosition = TextPosition( + offset: + sibling.getContainer().documentOffset + siblingPosition.offset); + } + } else { + newPosition = TextPosition( + offset: child.getContainer().documentOffset + newPosition.offset); + } + return newPosition; + } + + /// Returns the TextPosition below the given offset into the text. + /// + /// If the offset is already on the last line, the offset of the last + /// character will be returned. + @override + TextPosition getTextPositionBelow(TextPosition position) { + final child = childAtPosition(position); + final localPosition = TextPosition( + offset: position.offset - child.getContainer().documentOffset); + + var newPosition = child.getPositionBelow(localPosition); + + if (newPosition == null) { + // There was no text above in the current child, check the direct + // sibling. + final sibling = childAfter(child); + if (sibling == null) { + // reached beginning of the document, move to the + // last character + newPosition = TextPosition(offset: document.length - 1); + } else { + final caretOffset = child.getOffsetForCaret(localPosition); + const testPosition = TextPosition(offset: 0); + final testOffset = sibling.getOffsetForCaret(testPosition); + final finalOffset = Offset(caretOffset.dx, testOffset.dy); + final siblingPosition = sibling.getPositionForOffset(finalOffset); + newPosition = TextPosition( + offset: + sibling.getContainer().documentOffset + siblingPosition.offset); + } + } else { + newPosition = TextPosition( + offset: child.getContainer().documentOffset + newPosition.offset); + } + return newPosition; + } + + // End TextLayoutMetrics implementation + + @override + void systemFontsDidChange() { + super.systemFontsDidChange(); + markNeedsLayout(); + } + + void debugAssertLayoutUpToDate() { + // no-op? + // this assert was added by Flutter TextEditingActionTarge + // so we have to comply here. + } } class EditableContainerParentData diff --git a/lib/src/widgets/keyboard_listener.dart b/lib/src/widgets/keyboard_listener.dart deleted file mode 100644 index 4e755fdf..00000000 --- a/lib/src/widgets/keyboard_listener.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -//fixme workaround flutter MacOS issue https://github.com/flutter/flutter/issues/75595 -extension _LogicalKeyboardKeyCaseExt on LogicalKeyboardKey { - static const _kUpperToLowerDist = 0x20; - static final _kLowerCaseA = LogicalKeyboardKey.keyA.keyId; - static final _kLowerCaseZ = LogicalKeyboardKey.keyZ.keyId; - - LogicalKeyboardKey toUpperCase() { - if (keyId < _kLowerCaseA || keyId > _kLowerCaseZ) return this; - return LogicalKeyboardKey(keyId - _kUpperToLowerDist); - } -} - -enum InputShortcut { CUT, COPY, PASTE, SELECT_ALL, UNDO, REDO } - -typedef CursorMoveCallback = void Function( - LogicalKeyboardKey key, bool wordModifier, bool lineModifier, bool shift); -typedef InputShortcutCallback = void Function(InputShortcut? shortcut); -typedef OnDeleteCallback = void Function(bool forward); - -class KeyboardEventHandler { - KeyboardEventHandler(this.onCursorMove, this.onShortcut, this.onDelete); - - final CursorMoveCallback onCursorMove; - final InputShortcutCallback onShortcut; - final OnDeleteCallback onDelete; - - static final Set _moveKeys = { - LogicalKeyboardKey.arrowRight, - LogicalKeyboardKey.arrowLeft, - LogicalKeyboardKey.arrowUp, - LogicalKeyboardKey.arrowDown, - }; - - static final Set _shortcutKeys = { - LogicalKeyboardKey.keyA, - LogicalKeyboardKey.keyC, - LogicalKeyboardKey.keyV, - LogicalKeyboardKey.keyX, - LogicalKeyboardKey.keyZ.toUpperCase(), - LogicalKeyboardKey.keyZ, - LogicalKeyboardKey.delete, - LogicalKeyboardKey.backspace, - }; - - static final Set _nonModifierKeys = { - ..._shortcutKeys, - ..._moveKeys, - }; - - static final Set _modifierKeys = { - LogicalKeyboardKey.shift, - LogicalKeyboardKey.control, - LogicalKeyboardKey.alt, - }; - - static final Set _macOsModifierKeys = - { - LogicalKeyboardKey.shift, - LogicalKeyboardKey.meta, - LogicalKeyboardKey.alt, - }; - - static final Set _interestingKeys = { - ..._modifierKeys, - ..._macOsModifierKeys, - ..._nonModifierKeys, - }; - - static final Map _keyToShortcut = { - LogicalKeyboardKey.keyX: InputShortcut.CUT, - LogicalKeyboardKey.keyC: InputShortcut.COPY, - LogicalKeyboardKey.keyV: InputShortcut.PASTE, - LogicalKeyboardKey.keyA: InputShortcut.SELECT_ALL, - }; - - KeyEventResult handleRawKeyEvent(RawKeyEvent event) { - if (kIsWeb) { - // On web platform, we ignore the key because it's already processed. - return KeyEventResult.ignored; - } - - if (event is! RawKeyDownEvent) { - return KeyEventResult.ignored; - } - - final keysPressed = - LogicalKeyboardKey.collapseSynonyms(RawKeyboard.instance.keysPressed); - final key = event.logicalKey; - final isMacOS = event.data is RawKeyEventDataMacOs; - if (!_nonModifierKeys.contains(key) || - keysPressed - .difference(isMacOS ? _macOsModifierKeys : _modifierKeys) - .length > - 1 || - keysPressed.difference(_interestingKeys).isNotEmpty) { - return KeyEventResult.ignored; - } - - final isShortcutModifierPressed = - isMacOS ? event.isMetaPressed : event.isControlPressed; - - if (_moveKeys.contains(key)) { - onCursorMove( - key, - isMacOS ? event.isAltPressed : event.isControlPressed, - isMacOS ? event.isMetaPressed : event.isAltPressed, - event.isShiftPressed); - } else if (isShortcutModifierPressed && (_shortcutKeys.contains(key))) { - if (key == LogicalKeyboardKey.keyZ || - key == LogicalKeyboardKey.keyZ.toUpperCase()) { - onShortcut( - event.isShiftPressed ? InputShortcut.REDO : InputShortcut.UNDO); - } else { - onShortcut(_keyToShortcut[key]); - } - } else if (key == LogicalKeyboardKey.delete) { - onDelete(true); - } else if (key == LogicalKeyboardKey.backspace) { - onDelete(false); - } else { - return KeyEventResult.ignored; - } - return KeyEventResult.handled; - } -} diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index eef259f6..daa9563b 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -21,10 +21,8 @@ import 'cursor.dart'; import 'default_styles.dart'; import 'delegate.dart'; import 'editor.dart'; -import 'keyboard_listener.dart'; import 'proxy.dart'; import 'quill_single_child_scroll_view.dart'; -import 'raw_editor/raw_editor_state_keyboard_mixin.dart'; import 'raw_editor/raw_editor_state_selection_delegate_mixin.dart'; import 'raw_editor/raw_editor_state_text_input_client_mixin.dart'; import 'text_block.dart'; @@ -106,13 +104,11 @@ class RawEditorState extends EditorState AutomaticKeepAliveClientMixin, WidgetsBindingObserver, TickerProviderStateMixin, - RawEditorStateKeyboardMixin, + TextEditingActionTarget, RawEditorStateTextInputClientMixin, RawEditorStateSelectionDelegateMixin { final GlobalKey _editorKey = GlobalKey(); - // Keyboard - late KeyboardEventHandler _keyboardListener; KeyboardVisibilityController? _keyboardVisibilityController; StreamSubscription? _keyboardVisibilitySubscription; bool _keyboardVisible = false; @@ -370,13 +366,6 @@ class RawEditorState extends EditorState _floatingCursorResetController = AnimationController(vsync: this); _floatingCursorResetController.addListener(onFloatingCursorResetTick); - // Keyboard - _keyboardListener = KeyboardEventHandler( - handleCursorMovement, - handleShortcut, - handleDelete, - ); - if (defaultTargetPlatform == TargetPlatform.windows || defaultTargetPlatform == TargetPlatform.macOS || defaultTargetPlatform == TargetPlatform.linux || @@ -394,8 +383,7 @@ class RawEditorState extends EditorState }); } - _focusAttachment = widget.focusNode.attach(context, - onKey: (node, event) => _keyboardListener.handleRawKeyEvent(event)); + _focusAttachment = widget.focusNode.attach(context); widget.focusNode.addListener(_handleFocusChanged); } @@ -440,8 +428,7 @@ class RawEditorState extends EditorState if (widget.focusNode != oldWidget.focusNode) { oldWidget.focusNode.removeListener(_handleFocusChanged); _focusAttachment?.detach(); - _focusAttachment = widget.focusNode.attach(context, - onKey: (node, event) => _keyboardListener.handleRawKeyEvent(event)); + _focusAttachment = widget.focusNode.attach(context); widget.focusNode.addListener(_handleFocusChanged); updateKeepAlive(); } @@ -634,11 +621,6 @@ class RawEditorState extends EditorState return _editorKey.currentContext?.findRenderObject() as RenderEditor?; } - @override - TextEditingValue getTextEditingValue() { - return widget.controller.plainTextEditingValue; - } - @override void requestKeyboard() { if (_hasFocus) { @@ -652,11 +634,16 @@ class RawEditorState extends EditorState @override void setTextEditingValue( TextEditingValue value, SelectionChangedCause cause) { - if (value.text == textEditingValue.text) { - widget.controller.updateSelection(value.selection, ChangeSource.LOCAL); - } else { - _setEditingValue(value); + if (value == textEditingValue) { + return; } + textEditingValue = value; + userUpdateTextEditingValue(value, cause); + } + + @override + void debugAssertLayoutUpToDate() { + getRenderEditor()!.debugAssertLayoutUpToDate(); } // set editing value from clipboard for mobile @@ -731,12 +718,80 @@ class RawEditorState extends EditorState return true; } + @override + void copySelection(SelectionChangedCause cause) { + // Copied straight from EditableTextState + super.copySelection(cause); + if (cause == SelectionChangedCause.toolbar) { + bringIntoView(textEditingValue.selection.extent); + hideToolbar(false); + + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + break; + case TargetPlatform.macOS: + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + // Collapse the selection and hide the toolbar and handles. + userUpdateTextEditingValue( + TextEditingValue( + text: textEditingValue.text, + selection: TextSelection.collapsed( + offset: textEditingValue.selection.end), + ), + SelectionChangedCause.toolbar, + ); + break; + } + } + } + + @override + void cutSelection(SelectionChangedCause cause) { + // Copied straight from EditableTextState + super.cutSelection(cause); + if (cause == SelectionChangedCause.toolbar) { + bringIntoView(textEditingValue.selection.extent); + hideToolbar(); + } + } + + @override + Future pasteText(SelectionChangedCause cause) async { + // Copied straight from EditableTextState + super.pasteText(cause); // ignore: unawaited_futures + if (cause == SelectionChangedCause.toolbar) { + bringIntoView(textEditingValue.selection.extent); + hideToolbar(); + } + } + + @override + void selectAll(SelectionChangedCause cause) { + // Copied straight from EditableTextState + super.selectAll(cause); + if (cause == SelectionChangedCause.toolbar) { + bringIntoView(textEditingValue.selection.extent); + } + } + @override bool get wantKeepAlive => widget.focusNode.hasFocus; + @override + bool get obscureText => false; + + @override + bool get selectionEnabled => widget.enableInteractiveSelection; + @override bool get readOnly => widget.readOnly; + @override + TextLayoutMetrics get textLayoutMetrics => getRenderEditor()!; + @override AnimationController get floatingCursorResetController => _floatingCursorResetController; diff --git a/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart deleted file mode 100644 index 05dc0057..00000000 --- a/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart +++ /dev/null @@ -1,368 +0,0 @@ -import 'dart:ui'; - -import 'package:characters/characters.dart'; -import 'package:flutter/services.dart'; - -import '../../models/documents/document.dart'; -import '../../utils/diff_delta.dart'; -import '../editor.dart'; -import '../keyboard_listener.dart'; - -mixin RawEditorStateKeyboardMixin on EditorState { - // Holds the last cursor location the user selected in the case the user tries - // to select vertically past the end or beginning of the field. If they do, - // then we need to keep the old cursor location so that we can go back to it - // if they change their minds. Only used for moving selection up and down in a - // multiline text field when selecting using the keyboard. - int _cursorResetLocation = -1; - - // Whether we should reset the location of the cursor in the case the user - // tries to select vertically past the end or beginning of the field. If they - // do, then we need to keep the old cursor location so that we can go back to - // it if they change their minds. Only used for resetting selection up and - // down in a multiline text field when selecting using the keyboard. - bool _wasSelectingVerticallyWithKeyboard = false; - - void handleCursorMovement( - LogicalKeyboardKey key, - bool wordModifier, - bool lineModifier, - bool shift, - ) { - if (wordModifier && lineModifier) { - // If both modifiers are down, nothing happens on any of the platforms. - return; - } - final selection = widget.controller.selection; - - var newSelection = widget.controller.selection; - - final plainText = getTextEditingValue().text; - - final rightKey = key == LogicalKeyboardKey.arrowRight, - leftKey = key == LogicalKeyboardKey.arrowLeft, - upKey = key == LogicalKeyboardKey.arrowUp, - downKey = key == LogicalKeyboardKey.arrowDown; - - if ((rightKey || leftKey) && !(rightKey && leftKey)) { - newSelection = _jumpToBeginOrEndOfWord(newSelection, wordModifier, - leftKey, rightKey, plainText, lineModifier, shift); - } - - if (downKey || upKey) { - newSelection = _handleMovingCursorVertically( - upKey, downKey, shift, selection, newSelection, plainText); - } - - if (!shift) { - newSelection = - _placeCollapsedSelection(selection, newSelection, leftKey, rightKey); - } - - widget.controller.updateSelection(newSelection, ChangeSource.LOCAL); - } - - // Handles shortcut functionality including cut, copy, paste and select all - // using control/command + (X, C, V, A). - // TODO: Add support for formatting shortcuts: Cmd+B (bold), Cmd+I (italic) - // set editing value from clipboard for web - Future handleShortcut(InputShortcut? shortcut) async { - final selection = widget.controller.selection; - final plainText = getTextEditingValue().text; - if (shortcut == InputShortcut.COPY) { - if (!selection.isCollapsed) { - await Clipboard.setData( - ClipboardData(text: selection.textInside(plainText))); - } - return; - } - if (shortcut == InputShortcut.UNDO) { - if (widget.controller.hasUndo) { - widget.controller.undo(); - } - return; - } - if (shortcut == InputShortcut.REDO) { - if (widget.controller.hasRedo) { - widget.controller.redo(); - } - return; - } - if (shortcut == InputShortcut.CUT && !widget.readOnly) { - if (!selection.isCollapsed) { - final data = selection.textInside(plainText); - await Clipboard.setData(ClipboardData(text: data)); - - widget.controller.replaceText( - selection.start, - data.length, - '', - TextSelection.collapsed(offset: selection.start), - ); - - setTextEditingValue( - TextEditingValue( - text: selection.textBefore(plainText) + - selection.textAfter(plainText), - selection: TextSelection.collapsed(offset: selection.start), - ), - SelectionChangedCause.keyboard); - } - return; - } - if (shortcut == InputShortcut.PASTE && !widget.readOnly) { - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data != null) { - widget.controller.replaceText( - selection.start, - selection.end - selection.start, - data.text, - TextSelection.collapsed(offset: selection.start + data.text!.length), - ); - } - return; - } - if (shortcut == InputShortcut.SELECT_ALL && - widget.enableInteractiveSelection) { - widget.controller.updateSelection( - selection.copyWith( - baseOffset: 0, - extentOffset: getTextEditingValue().text.length, - ), - ChangeSource.REMOTE); - return; - } - } - - void handleDelete(bool forward) { - final selection = widget.controller.selection; - final plainText = getTextEditingValue().text; - var cursorPosition = selection.start; - var textBefore = selection.textBefore(plainText); - var textAfter = selection.textAfter(plainText); - if (selection.isCollapsed) { - if (!forward && textBefore.isNotEmpty) { - final characterBoundary = - _previousCharacter(textBefore.length, textBefore, true); - textBefore = textBefore.substring(0, characterBoundary); - cursorPosition = characterBoundary; - } - if (forward && textAfter.isNotEmpty && textAfter != '\n') { - final deleteCount = _nextCharacter(0, textAfter, true); - textAfter = textAfter.substring(deleteCount); - } - } - final newSelection = TextSelection.collapsed(offset: cursorPosition); - final newText = textBefore + textAfter; - final size = plainText.length - newText.length; - if (size == 0) { - widget.controller.handleDelete(cursorPosition, forward); - } else { - widget.controller.replaceText(cursorPosition, size, '', newSelection); - } - } - - TextSelection _jumpToBeginOrEndOfWord( - TextSelection newSelection, - bool wordModifier, - bool leftKey, - bool rightKey, - String plainText, - bool lineModifier, - bool shift) { - if (wordModifier) { - if (leftKey) { - final textSelection = getRenderEditor()!.selectWordAtPosition( - TextPosition( - offset: _previousCharacter( - newSelection.extentOffset, plainText, false))); - return newSelection.copyWith(extentOffset: textSelection.baseOffset); - } - final textSelection = getRenderEditor()!.selectWordAtPosition( - TextPosition( - offset: - _nextCharacter(newSelection.extentOffset, plainText, false))); - return newSelection.copyWith(extentOffset: textSelection.extentOffset); - } else if (lineModifier) { - if (leftKey) { - final textSelection = getRenderEditor()!.selectLineAtPosition( - TextPosition( - offset: _previousCharacter( - newSelection.extentOffset, plainText, false))); - return newSelection.copyWith(extentOffset: textSelection.baseOffset); - } - final startPoint = newSelection.extentOffset; - if (startPoint < plainText.length) { - final textSelection = getRenderEditor()! - .selectLineAtPosition(TextPosition(offset: startPoint)); - return newSelection.copyWith(extentOffset: textSelection.extentOffset); - } - return newSelection; - } - - if (rightKey && newSelection.extentOffset < plainText.length) { - final nextExtent = - _nextCharacter(newSelection.extentOffset, plainText, true); - final distance = nextExtent - newSelection.extentOffset; - newSelection = newSelection.copyWith(extentOffset: nextExtent); - if (shift) { - _cursorResetLocation += distance; - } - return newSelection; - } - - if (leftKey && newSelection.extentOffset > 0) { - final previousExtent = - _previousCharacter(newSelection.extentOffset, plainText, true); - final distance = newSelection.extentOffset - previousExtent; - newSelection = newSelection.copyWith(extentOffset: previousExtent); - if (shift) { - _cursorResetLocation -= distance; - } - return newSelection; - } - return newSelection; - } - - /// Returns the index into the string of the next character boundary after the - /// given index. - /// - /// The character boundary is determined by the characters package, so - /// surrogate pairs and extended grapheme clusters are considered. - /// - /// The index must be between 0 and string.length, inclusive. If given - /// string.length, string.length is returned. - /// - /// Setting includeWhitespace to false will only return the index of non-space - /// characters. - int _nextCharacter(int index, String string, bool includeWhitespace) { - assert(index >= 0 && index <= string.length); - if (index == string.length) { - return string.length; - } - - var count = 0; - final remain = string.characters.skipWhile((currentString) { - if (count <= index) { - count += currentString.length; - return true; - } - if (includeWhitespace) { - return false; - } - return WHITE_SPACE.contains(currentString.codeUnitAt(0)); - }); - return string.length - remain.toString().length; - } - - /// Returns the index into the string of the previous character boundary - /// before the given index. - /// - /// The character boundary is determined by the characters package, so - /// surrogate pairs and extended grapheme clusters are considered. - /// - /// The index must be between 0 and string.length, inclusive. If index is 0, - /// 0 will be returned. - /// - /// Setting includeWhitespace to false will only return the index of non-space - /// characters. - int _previousCharacter(int index, String string, includeWhitespace) { - assert(index >= 0 && index <= string.length); - if (index == 0) { - return 0; - } - - var count = 0; - int? lastNonWhitespace; - for (final currentString in string.characters) { - if (!includeWhitespace && - !WHITE_SPACE.contains( - currentString.characters.first.toString().codeUnitAt(0))) { - lastNonWhitespace = count; - } - if (count + currentString.length >= index) { - return includeWhitespace ? count : lastNonWhitespace ?? 0; - } - count += currentString.length; - } - return 0; - } - - TextSelection _handleMovingCursorVertically( - bool upKey, - bool downKey, - bool shift, - TextSelection selection, - TextSelection newSelection, - String plainText) { - final originPosition = TextPosition( - offset: upKey ? selection.baseOffset : selection.extentOffset); - - final child = getRenderEditor()!.childAtPosition(originPosition); - final localPosition = TextPosition( - offset: originPosition.offset - child.getContainer().documentOffset); - - var position = upKey - ? child.getPositionAbove(localPosition) - : child.getPositionBelow(localPosition); - - if (position == null) { - final sibling = upKey - ? getRenderEditor()!.childBefore(child) - : getRenderEditor()!.childAfter(child); - if (sibling == null) { - position = TextPosition(offset: upKey ? 0 : plainText.length - 1); - } else { - final finalOffset = Offset( - child.getOffsetForCaret(localPosition).dx, - sibling - .getOffsetForCaret(TextPosition( - offset: upKey ? sibling.getContainer().length - 1 : 0)) - .dy); - final siblingPosition = sibling.getPositionForOffset(finalOffset); - position = TextPosition( - offset: - sibling.getContainer().documentOffset + siblingPosition.offset); - } - } else { - position = TextPosition( - offset: child.getContainer().documentOffset + position.offset); - } - - if (position.offset == newSelection.extentOffset) { - if (downKey) { - newSelection = newSelection.copyWith(extentOffset: plainText.length); - } else if (upKey) { - newSelection = newSelection.copyWith(extentOffset: 0); - } - _wasSelectingVerticallyWithKeyboard = shift; - return newSelection; - } - - if (_wasSelectingVerticallyWithKeyboard && shift) { - newSelection = newSelection.copyWith(extentOffset: _cursorResetLocation); - _wasSelectingVerticallyWithKeyboard = false; - return newSelection; - } - newSelection = newSelection.copyWith(extentOffset: position.offset); - _cursorResetLocation = newSelection.extentOffset; - return newSelection; - } - - TextSelection _placeCollapsedSelection(TextSelection selection, - TextSelection newSelection, bool leftKey, bool rightKey) { - var newOffset = newSelection.extentOffset; - if (!selection.isCollapsed) { - if (leftKey) { - newOffset = newSelection.baseOffset < newSelection.extentOffset - ? newSelection.baseOffset - : newSelection.extentOffset; - } else if (rightKey) { - newOffset = newSelection.baseOffset > newSelection.extentOffset - ? newSelection.baseOffset - : newSelection.extentOffset; - } - } - return TextSelection.fromPosition(TextPosition(offset: newOffset)); - } -} 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 0ee0ed66..36a5b5b2 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 @@ -1,9 +1,8 @@ import 'dart:math' as math; -import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import '../../../utils/diff_delta.dart'; import '../editor.dart'; @@ -11,13 +10,17 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState implements TextSelectionDelegate { @override TextEditingValue get textEditingValue { - return getTextEditingValue(); + return widget.controller.plainTextEditingValue; } @override set textEditingValue(TextEditingValue value) { - // deprecated - setTextEditingValue(value, SelectionChangedCause.keyboard); + final cursorPosition = value.selection.extentOffset; + final oldText = widget.controller.document.toPlainText(); + final newText = value.text; + final diff = getDiff(oldText, newText, cursorPosition); + widget.controller.replaceText( + diff.start, diff.deleted.length, diff.inserted, value.selection); } @override @@ -85,7 +88,7 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState @override void userUpdateTextEditingValue( TextEditingValue value, SelectionChangedCause cause) { - setTextEditingValue(value, cause); + textEditingValue = value; } @override @@ -99,119 +102,4 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState @override bool get selectAllEnabled => widget.toolbarOptions.selectAll; - - void setSelection(TextSelection nextSelection, SelectionChangedCause cause) { - if (nextSelection == textEditingValue.selection) { - return; - } - setTextEditingValue( - textEditingValue.copyWith(selection: nextSelection), - cause, - ); - } - - @override - void copySelection(SelectionChangedCause cause) { - final selection = textEditingValue.selection; - if (selection.isCollapsed || !selection.isValid) { - return; - } - Clipboard.setData( - ClipboardData(text: selection.textInside(textEditingValue.text))); - - if (cause == SelectionChangedCause.toolbar) { - bringIntoView(textEditingValue.selection.extent); - hideToolbar(false); - - switch (defaultTargetPlatform) { - case TargetPlatform.iOS: - break; - case TargetPlatform.macOS: - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.windows: - // Collapse the selection and hide the toolbar and handles. - userUpdateTextEditingValue( - TextEditingValue( - text: textEditingValue.text, - selection: TextSelection.collapsed( - offset: textEditingValue.selection.end), - ), - SelectionChangedCause.toolbar, - ); - break; - } - } - } - - @override - void cutSelection(SelectionChangedCause cause) { - final selection = textEditingValue.selection; - if (readOnly || !selection.isValid || selection.isCollapsed) { - return; - } - final text = textEditingValue.text; - Clipboard.setData(ClipboardData(text: selection.textInside(text))); - setTextEditingValue( - TextEditingValue( - text: selection.textBefore(text) + selection.textAfter(text), - selection: TextSelection.collapsed( - offset: math.min(selection.start, selection.end), - affinity: selection.affinity, - ), - ), - cause, - ); - - if (cause == SelectionChangedCause.toolbar) { - bringIntoView(textEditingValue.selection.extent); - hideToolbar(); - } - } - - @override - Future pasteText(SelectionChangedCause cause) async { - final selection = textEditingValue.selection; - if (readOnly || !selection.isValid) { - return; - } - final text = textEditingValue.text; - // See https://github.com/flutter/flutter/issues/11427 - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data == null) { - return; - } - setTextEditingValue( - TextEditingValue( - text: - selection.textBefore(text) + data.text! + selection.textAfter(text), - selection: TextSelection.collapsed( - offset: math.min(selection.start, selection.end) + data.text!.length, - affinity: selection.affinity, - ), - ), - cause, - ); - - if (cause == SelectionChangedCause.toolbar) { - bringIntoView(textEditingValue.selection.extent); - hideToolbar(); - } - } - - @override - void selectAll(SelectionChangedCause cause) { - setSelection( - textEditingValue.selection.copyWith( - baseOffset: 0, - extentOffset: textEditingValue.text.length, - ), - cause, - ); - - if (cause == SelectionChangedCause.toolbar) { - bringIntoView(textEditingValue.selection.extent); - } - } } diff --git a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart index cfb13e62..c0443349 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart @@ -49,7 +49,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState } if (!hasConnection) { - _lastKnownRemoteTextEditingValue = getTextEditingValue(); + _lastKnownRemoteTextEditingValue = textEditingValue; _textInputConnection = TextInput.attach( this, TextInputConfiguration( @@ -90,12 +90,14 @@ mixin RawEditorStateTextInputClientMixin on EditorState return; } + final value = textEditingValue; + // Since we don't keep track of the composing range in value provided // by the Controller we need to add it here manually before comparing // with the last known remote value. // It is important to prevent excessive remote updates as it can cause // race conditions. - final actualValue = getTextEditingValue().copyWith( + final actualValue = value.copyWith( composing: _lastKnownRemoteTextEditingValue!.composing, ); @@ -103,8 +105,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState return; } - final shouldRemember = - getTextEditingValue().text != _lastKnownRemoteTextEditingValue!.text; + final shouldRemember = value.text != _lastKnownRemoteTextEditingValue!.text; _lastKnownRemoteTextEditingValue = actualValue; _textInputConnection!.setEditingState( // Set composing to (-1, -1), otherwise an exception will be thrown if diff --git a/lib/widgets/keyboard_listener.dart b/lib/widgets/keyboard_listener.dart deleted file mode 100644 index 20c72b74..00000000 --- a/lib/widgets/keyboard_listener.dart +++ /dev/null @@ -1,3 +0,0 @@ -/// TODO: Remove this file in the next breaking release, because implementation -/// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../src/widgets/keyboard_listener.dart'; 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 ab326cec..00000000 --- a/lib/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart +++ /dev/null @@ -1,3 +0,0 @@ -/// TODO: Remove this file in the next breaking release, because implementation -/// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart'; From 46f29a2a9b2acdcaf88e830a296543eb9c7f8e2c Mon Sep 17 00:00:00 2001 From: X Code Date: Fri, 10 Dec 2021 08:55:55 -0800 Subject: [PATCH 114/179] Upgrade to 2.3.0 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1ba734c..e99a9fc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.3.0] +* Massive changes to support flutter 2.8. + ## [2.2.2] * iOS - floating cursor. diff --git a/pubspec.yaml b/pubspec.yaml index 5a5a6bcb..025589a0 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: 2.2.2 +version: 2.3.0 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From e5fe8a98719e0d34bd26b60189b837a06f49d050 Mon Sep 17 00:00:00 2001 From: X Code Date: Fri, 10 Dec 2021 08:57:06 -0800 Subject: [PATCH 115/179] Format code --- lib/src/widgets/raw_editor.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index daa9563b..9502060a 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -734,7 +734,7 @@ class RawEditorState extends EditorState case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: - // Collapse the selection and hide the toolbar and handles. + // Collapse the selection and hide the toolbar and handles. userUpdateTextEditingValue( TextEditingValue( text: textEditingValue.text, From 8bd4f1ee642b9ebf14af3f7fb2c0d493dcdd00a4 Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 13 Dec 2021 21:57:57 -0800 Subject: [PATCH 116/179] Preserve last newline character on delete Occurs on desktop when DEL is pressed at the end of the document (attempting to delete the last newline character). --- lib/src/models/rules/delete.dart | 28 +++++++++++++++++++++++++++- lib/src/models/rules/rule.dart | 1 + 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/src/models/rules/delete.dart b/lib/src/models/rules/delete.dart index 06c057eb..91e884d9 100644 --- a/lib/src/models/rules/delete.dart +++ b/lib/src/models/rules/delete.dart @@ -16,15 +16,33 @@ abstract class DeleteRule extends Rule { } } +class EnsureLastLineBreakDeleteRule extends DeleteRule { + const EnsureLastLineBreakDeleteRule(); + + @override + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + final itr = DeltaIterator(document)..skip(index + len!); + + return Delta() + ..retain(index) + ..delete(itr.hasNext ? len : len - 1); + } +} + +/// Fallback rule for delete operations which simply deletes specified text +/// range without any special handling. class CatchAllDeleteRule extends DeleteRule { const CatchAllDeleteRule(); @override Delta applyRule(Delta document, int index, {int? len, Object? data, Attribute? attribute}) { + final itr = DeltaIterator(document)..skip(index + len!); + return Delta() ..retain(index) - ..delete(len!); + ..delete(itr.hasNext ? len : len - 1); } } @@ -44,6 +62,14 @@ class PreserveLineStyleOnMergeRule extends DeleteRule { final attrs = op.attributes; itr.skip(len! - 1); + + if (!itr.hasNext) { + // User attempts to delete the last newline character, prevent it. + return Delta() + ..retain(index) + ..delete(len - 1); + } + final delta = Delta() ..retain(index) ..delete(len); diff --git a/lib/src/models/rules/rule.dart b/lib/src/models/rules/rule.dart index a2649499..c9a0f911 100644 --- a/lib/src/models/rules/rule.dart +++ b/lib/src/models/rules/rule.dart @@ -46,6 +46,7 @@ class Rules { const EnsureEmbedLineRule(), const PreserveLineStyleOnMergeRule(), const CatchAllDeleteRule(), + const EnsureLastLineBreakDeleteRule() ]); static Rules getInstance() => _instance; From 2703fa67423a0ddde324673db471e6b8b0de58ab Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 13 Dec 2021 21:59:05 -0800 Subject: [PATCH 117/179] Upgrade to 2.3.1 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e99a9fc0..1a7c42f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.3.1] +* Preserve last newline character on delete. + ## [2.3.0] * Massive changes to support flutter 2.8. diff --git a/pubspec.yaml b/pubspec.yaml index 025589a0..ea48a481 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: 2.3.0 +version: 2.3.1 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 69f64047b58cb61cc472d10f9eabc43eabcc5c4e Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 13 Dec 2021 23:04:55 -0800 Subject: [PATCH 118/179] Upgrade flutter_colorpicker to 1.0.3 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index ea48a481..2cf447f4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: flutter: sdk: flutter collection: ^1.15.0 - flutter_colorpicker: ^0.6.0 + flutter_colorpicker: ^1.0.3 flutter_keyboard_visibility: ^5.0.0 image_picker: ^0.8.2 photo_view: ^0.13.0 From 6961542216bfea1be6acb8b16731da40162a7fa0 Mon Sep 17 00:00:00 2001 From: Andy Trand Date: Tue, 14 Dec 2021 11:33:32 +0200 Subject: [PATCH 119/179] disable floating cursor (#526) --- lib/src/widgets/editor.dart | 10 +++++++++- lib/src/widgets/raw_editor.dart | 10 +++++++++- lib/src/widgets/simple_viewer.dart | 6 +++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 94869f8d..0cbffe10 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -242,6 +242,7 @@ class QuillEditor extends StatefulWidget { this.onSingleLongTapEnd, this.embedBuilder = defaultEmbedBuilder, this.customStyleBuilder, + this.floatingCursorDisabled = false, Key? key}); factory QuillEditor.basic({ @@ -306,6 +307,8 @@ class QuillEditor extends StatefulWidget { final EmbedBuilder embedBuilder; final CustomStyleBuilder? customStyleBuilder; + final bool floatingCursorDisabled; + @override _QuillEditorState createState() => _QuillEditorState(); } @@ -408,6 +411,7 @@ class _QuillEditorState extends State scrollPhysics: widget.scrollPhysics, embedBuilder: widget.embedBuilder, customStyleBuilder: widget.customStyleBuilder, + floatingCursorDisabled: widget.floatingCursorDisabled, ); return _selectionGestureDetectorBuilder.build( @@ -692,7 +696,8 @@ class RenderEditor extends RenderEditableContainerBox this._startHandleLayerLink, this._endHandleLayerLink, EdgeInsets floatingCursorAddedMargin, - this._cursorController) + this._cursorController, + this.floatingCursorDisabled) : super( children, document.root, @@ -702,6 +707,7 @@ class RenderEditor extends RenderEditableContainerBox ); final CursorCont _cursorController; + final bool floatingCursorDisabled; Document document; TextSelection selection; @@ -1214,6 +1220,8 @@ class RenderEditor extends RenderEditableContainerBox void setFloatingCursor(FloatingCursorDragState dragState, Offset boundedOffset, TextPosition textPosition, {double? resetLerpValue}) { + if (floatingCursorDisabled) return; + if (dragState == FloatingCursorDragState.Start) { _relativeOrigin = Offset.zero; _previousOffset = null; diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 9502060a..c42b95f6 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -63,6 +63,7 @@ class RawEditor extends StatefulWidget { this.scrollPhysics, this.embedBuilder = defaultEmbedBuilder, this.customStyleBuilder, + this.floatingCursorDisabled = false }) : assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'), assert(minHeight == null || minHeight >= 0, 'minHeight cannot be null'), assert(maxHeight == null || minHeight == null || maxHeight >= minHeight, @@ -95,6 +96,8 @@ class RawEditor extends StatefulWidget { final ScrollPhysics? scrollPhysics; final EmbedBuilder embedBuilder; final CustomStyleBuilder? customStyleBuilder; + final bool floatingCursorDisabled; + @override State createState() => RawEditorState(); } @@ -167,6 +170,7 @@ class RawEditorState extends EditorState onSelectionChanged: _handleSelectionChanged, scrollBottomInset: widget.scrollBottomInset, padding: widget.padding, + floatingCursorDisabled: widget.floatingCursorDisabled, children: _buildChildren(_doc, context), ), ), @@ -196,6 +200,7 @@ class RawEditorState extends EditorState scrollBottomInset: widget.scrollBottomInset, padding: widget.padding, cursorController: _cursorCont, + floatingCursorDisabled: widget.floatingCursorDisabled, children: _buildChildren(_doc, context), ), ), @@ -812,6 +817,7 @@ class _Editor extends MultiChildRenderObjectWidget { required this.onSelectionChanged, required this.scrollBottomInset, required this.cursorController, + required this.floatingCursorDisabled, this.padding = EdgeInsets.zero, this.offset, }) : super(key: key, children: children); @@ -827,6 +833,7 @@ class _Editor extends MultiChildRenderObjectWidget { final double scrollBottomInset; final EdgeInsetsGeometry padding; final CursorCont cursorController; + final bool floatingCursorDisabled; @override RenderEditor createRenderObject(BuildContext context) { @@ -843,7 +850,8 @@ class _Editor extends MultiChildRenderObjectWidget { startHandleLayerLink, endHandleLayerLink, const EdgeInsets.fromLTRB(4, 4, 4, 5), - cursorController); + cursorController, + floatingCursorDisabled); } @override diff --git a/lib/src/widgets/simple_viewer.dart b/lib/src/widgets/simple_viewer.dart index 9a2dc221..ab749063 100644 --- a/lib/src/widgets/simple_viewer.dart +++ b/lib/src/widgets/simple_viewer.dart @@ -158,6 +158,7 @@ class _QuillSimpleViewerState extends State scrollBottomInset: widget.scrollBottomInset, padding: widget.padding, cursorController: _cursorCont, + floatingCursorDisabled: true, children: _buildChildren(_doc, context)), ), ); @@ -316,6 +317,7 @@ class _SimpleViewer extends MultiChildRenderObjectWidget { required this.onSelectionChanged, required this.scrollBottomInset, required this.cursorController, + required this.floatingCursorDisabled, this.offset, this.padding = EdgeInsets.zero, Key? key, @@ -330,6 +332,7 @@ class _SimpleViewer extends MultiChildRenderObjectWidget { final double scrollBottomInset; final EdgeInsetsGeometry padding; final CursorCont cursorController; + final bool floatingCursorDisabled; @override RenderEditor createRenderObject(BuildContext context) { @@ -347,7 +350,8 @@ class _SimpleViewer extends MultiChildRenderObjectWidget { startHandleLayerLink, endHandleLayerLink, const EdgeInsets.fromLTRB(4, 4, 4, 5), - cursorController); + cursorController, + floatingCursorDisabled); } @override From 156e0d71df069cb55b2f54f62003a38ba1b7f442 Mon Sep 17 00:00:00 2001 From: X Code Date: Tue, 14 Dec 2021 08:55:15 -0800 Subject: [PATCH 120/179] Upgrade to 2.3.2 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a7c42f8..6fb4d24a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.3.2] +* Allow disabling floating cursor. + ## [2.3.1] * Preserve last newline character on delete. diff --git a/pubspec.yaml b/pubspec.yaml index 2cf447f4..335198e2 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: 2.3.1 +version: 2.3.2 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From d1fa59a759137550f4e74d24a38b4a40b833afba Mon Sep 17 00:00:00 2001 From: X Code Date: Tue, 14 Dec 2021 15:21:07 -0800 Subject: [PATCH 121/179] Reformat code --- lib/src/widgets/raw_editor.dart | 70 ++++++++++++++++----------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index c42b95f6..01b3a351 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -30,41 +30,41 @@ import 'text_line.dart'; import 'text_selection.dart'; class RawEditor extends StatefulWidget { - const RawEditor({ - required this.controller, - required this.focusNode, - required this.scrollController, - required this.scrollBottomInset, - required this.cursorStyle, - required this.selectionColor, - required this.selectionCtrls, - Key? key, - this.scrollable = true, - this.padding = EdgeInsets.zero, - this.readOnly = false, - this.placeholder, - this.onLaunchUrl, - this.toolbarOptions = const ToolbarOptions( - copy: true, - cut: true, - paste: true, - selectAll: true, - ), - this.showSelectionHandles = false, - bool? showCursor, - this.textCapitalization = TextCapitalization.none, - this.maxHeight, - this.minHeight, - this.customStyles, - this.expands = false, - this.autoFocus = false, - this.keyboardAppearance = Brightness.light, - this.enableInteractiveSelection = true, - this.scrollPhysics, - this.embedBuilder = defaultEmbedBuilder, - this.customStyleBuilder, - this.floatingCursorDisabled = false - }) : assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'), + const RawEditor( + {required this.controller, + required this.focusNode, + required this.scrollController, + required this.scrollBottomInset, + required this.cursorStyle, + required this.selectionColor, + required this.selectionCtrls, + Key? key, + this.scrollable = true, + this.padding = EdgeInsets.zero, + this.readOnly = false, + this.placeholder, + this.onLaunchUrl, + this.toolbarOptions = const ToolbarOptions( + copy: true, + cut: true, + paste: true, + selectAll: true, + ), + this.showSelectionHandles = false, + bool? showCursor, + this.textCapitalization = TextCapitalization.none, + this.maxHeight, + this.minHeight, + this.customStyles, + this.expands = false, + this.autoFocus = false, + this.keyboardAppearance = Brightness.light, + this.enableInteractiveSelection = true, + this.scrollPhysics, + this.embedBuilder = defaultEmbedBuilder, + this.customStyleBuilder, + this.floatingCursorDisabled = false}) + : assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'), assert(minHeight == null || minHeight >= 0, 'minHeight cannot be null'), assert(maxHeight == null || minHeight == null || maxHeight >= minHeight, 'maxHeight cannot be null'), From e9b119299a3fe98a276fea3ac2b5a0a06df3806e Mon Sep 17 00:00:00 2001 From: X Code Date: Tue, 14 Dec 2021 22:22:59 -0800 Subject: [PATCH 122/179] Improves selection rects to have consistent height regardless of individual segment text styles Fixes an issue where editable text line would stay subscribed to cursor events. --- lib/src/widgets/proxy.dart | 6 ++++-- lib/src/widgets/text_line.dart | 10 ++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/src/widgets/proxy.dart b/lib/src/widgets/proxy.dart index a1cb8c6a..45a513b2 100644 --- a/lib/src/widgets/proxy.dart +++ b/lib/src/widgets/proxy.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -291,8 +293,8 @@ class RenderParagraphProxy extends RenderProxyBox child!.getWordBoundary(position); @override - List getBoxesForSelection(TextSelection selection) => - child!.getBoxesForSelection(selection); + List getBoxesForSelection(TextSelection selection) => child! + .getBoxesForSelection(selection, boxHeightStyle: BoxHeightStyle.strut); @override void performLayout() { diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index 5e365272..d2762f7c 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -441,9 +441,10 @@ class RenderEditableTextLine extends RenderEditableBox { } final containsSelection = containsTextSelection(); - if (attached && containsCursor()) { + if (_attachedToCursorController) { cursorCont.removeListener(markNeedsLayout); cursorCont.color.removeListener(safeMarkNeedsPaint); + _attachedToCursorController = false; } textSelection = t; @@ -452,6 +453,7 @@ class RenderEditableTextLine extends RenderEditableBox { if (attached && containsCursor()) { cursorCont.addListener(markNeedsLayout); cursorCont.color.addListener(safeMarkNeedsPaint); + _attachedToCursorController = true; } if (containsSelection || containsTextSelection()) { @@ -680,6 +682,8 @@ class RenderEditableTextLine extends RenderEditableBox { // Start render box overrides + bool _attachedToCursorController = false; + @override void attach(covariant PipelineOwner owner) { super.attach(owner); @@ -690,6 +694,7 @@ class RenderEditableTextLine extends RenderEditableBox { if (containsCursor()) { cursorCont.addListener(markNeedsLayout); cursorCont.color.addListener(safeMarkNeedsPaint); + _attachedToCursorController = true; } } @@ -701,9 +706,10 @@ class RenderEditableTextLine extends RenderEditableBox { } cursorCont.floatingCursorTextPosition .removeListener(_onFloatingCursorChange); - if (containsCursor()) { + if (_attachedToCursorController) { cursorCont.removeListener(markNeedsLayout); cursorCont.color.removeListener(safeMarkNeedsPaint); + _attachedToCursorController = false; } } From 910c1c7487a24aa5a94676e9b94a9bfc76d57fbb Mon Sep 17 00:00:00 2001 From: X Code Date: Tue, 14 Dec 2021 22:32:38 -0800 Subject: [PATCH 123/179] Upgrade to 2.3.3 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fb4d24a..9e3b163c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.3.3] +* Improves selection rects to have consistent height regardless of individual segment text styles. + ## [2.3.2] * Allow disabling floating cursor. diff --git a/pubspec.yaml b/pubspec.yaml index 335198e2..d9ea96ad 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: 2.3.2 +version: 2.3.3 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 919ff0ec07f2ed686c4626671b678c8dd7e7dc59 Mon Sep 17 00:00:00 2001 From: X Code Date: Tue, 14 Dec 2021 23:26:43 -0800 Subject: [PATCH 124/179] [2.4.0] Improve inline code style --- CHANGELOG.md | 3 + lib/src/widgets/default_styles.dart | 105 ++++++++++++++++++++++++++-- lib/src/widgets/text_line.dart | 78 ++++++++++++++++----- 3 files changed, 163 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e3b163c..2a4bd971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.4.0] +* Improve inline code style. + ## [2.3.3] * Improves selection rects to have consistent height regardless of individual segment text styles. diff --git a/lib/src/widgets/default_styles.dart b/lib/src/widgets/default_styles.dart index af1469c1..dab9cc03 100644 --- a/lib/src/widgets/default_styles.dart +++ b/lib/src/widgets/default_styles.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; -import 'style_widgets/style_widgets.dart'; +import '../../flutter_quill.dart'; +import '../../models/documents/style.dart'; class QuillStyles extends InheritedWidget { const QuillStyles({ @@ -27,6 +28,8 @@ class QuillStyles extends InheritedWidget { } } +/// Style theme applied to a block of rich text, including single-line +/// paragraphs. class DefaultTextBlockStyle { DefaultTextBlockStyle( this.style, @@ -35,15 +38,88 @@ class DefaultTextBlockStyle { this.decoration, ); + /// Base text style for a text block. final TextStyle style; + /// Vertical spacing around a text block. final Tuple2 verticalSpacing; + /// Vertical spacing for individual lines within a text block. + /// final Tuple2 lineSpacing; + /// Decoration of a text block. + /// + /// Decoration, if present, is painted in the content area, excluding + /// any [spacing]. final BoxDecoration? decoration; } +/// Theme data for inline code. +class InlineCodeStyle { + InlineCodeStyle({ + required this.style, + this.heading1, + this.heading2, + this.heading3, + this.backgroundColor, + this.radius, + }); + + /// Base text style for an inline code. + final TextStyle style; + + /// Style override for inline code in headings level 1. + final TextStyle? heading1; + + /// Style override for inline code in headings level 2. + final TextStyle? heading2; + + /// Style override for inline code in headings level 3. + final TextStyle? heading3; + + /// Background color for inline code. + final Color? backgroundColor; + + /// Radius used when paining the background. + final Radius? radius; + + /// Returns effective style to use for inline code for the specified + /// [lineStyle]. + TextStyle styleFor(Style lineStyle) { + if (lineStyle.containsKey(Attribute.h1.key)) { + return heading1 ?? style; + } + if (lineStyle.containsKey(Attribute.h2.key)) { + return heading2 ?? style; + } + if (lineStyle.containsKey(Attribute.h3.key)) { + return heading3 ?? style; + } + return style; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other is! InlineCodeStyle) { + return false; + } + return other.style == style && + other.heading1 == heading1 && + other.heading2 == heading2 && + other.heading3 == heading3 && + other.backgroundColor == backgroundColor && + other.radius == radius; + } + + @override + int get hashCode => + Object.hash(style, heading1, heading2, heading3, backgroundColor, radius); +} + class DefaultListBlockStyle extends DefaultTextBlockStyle { DefaultListBlockStyle( TextStyle style, @@ -91,7 +167,9 @@ class DefaultStyles { final TextStyle? small; final TextStyle? underline; final TextStyle? strikeThrough; - final TextStyle? inlineCode; + + /// Theme of inline code. + final InlineCodeStyle? inlineCode; final TextStyle? sizeSmall; // 'small' final TextStyle? sizeLarge; // 'large' final TextStyle? sizeHuge; // 'huge' @@ -129,6 +207,12 @@ class DefaultStyles { throw UnimplementedError(); } + final inlineCodeStyle = TextStyle( + fontSize: 14, + color: themeData.colorScheme.primaryVariant.withOpacity(0.8), + fontFamily: fontFamily, + ); + return DefaultStyles( h1: DefaultTextBlockStyle( defaultTextStyle.style.copyWith( @@ -167,10 +251,19 @@ class DefaultStyles { small: const TextStyle(fontSize: 12, color: Colors.black45), underline: const TextStyle(decoration: TextDecoration.underline), strikeThrough: const TextStyle(decoration: TextDecoration.lineThrough), - inlineCode: TextStyle( - color: Colors.blue.shade900.withOpacity(0.9), - fontFamily: fontFamily, - fontSize: 13, + inlineCode: InlineCodeStyle( + backgroundColor: Colors.grey.shade100, + radius: const Radius.circular(3), + style: inlineCodeStyle, + heading1: inlineCodeStyle.copyWith( + fontSize: 32, + fontWeight: FontWeight.w300, + ), + heading2: inlineCodeStyle.copyWith(fontSize: 22), + heading3: inlineCodeStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.w500, + ), ), link: TextStyle( color: themeData.colorScheme.secondary, diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index d2762f7c..d75daffd 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -12,6 +12,7 @@ import '../models/documents/nodes/leaf.dart' as leaf; import '../models/documents/nodes/leaf.dart'; import '../models/documents/nodes/line.dart'; import '../models/documents/nodes/node.dart'; +import '../models/documents/style.dart'; import '../utils/color.dart'; import 'box.dart'; import 'cursor.dart'; @@ -118,7 +119,7 @@ class TextLine extends StatelessWidget { TextSpan _buildTextSpan(DefaultStyles defaultStyles, LinkedList nodes, TextStyle lineStyle) { final children = nodes - .map((node) => _getTextSpanFromNode(defaultStyles, node)) + .map((node) => _getTextSpanFromNode(defaultStyles, node, line.style)) .toList(growable: false); return TextSpan(children: children, style: lineStyle); @@ -179,9 +180,10 @@ class TextLine extends StatelessWidget { return textStyle; } - TextSpan _getTextSpanFromNode(DefaultStyles defaultStyles, Node node) { + TextSpan _getTextSpanFromNode( + DefaultStyles defaultStyles, Node node, Style lineStyle) { final textNode = node as leaf.Text; - final style = textNode.style; + final nodeStyle = textNode.style; var res = const TextStyle(); // This is inline text style final color = textNode.style.attributes[Attribute.color.key]; var hasLink = false; @@ -193,9 +195,8 @@ class TextLine extends StatelessWidget { Attribute.link.key: defaultStyles.link, Attribute.underline.key: defaultStyles.underline, Attribute.strikeThrough.key: defaultStyles.strikeThrough, - Attribute.inlineCode.key: defaultStyles.inlineCode, }.forEach((k, s) { - if (style.values.any((v) => v.key == k)) { + if (nodeStyle.values.any((v) => v.key == k)) { if (k == Attribute.underline.key || k == Attribute.strikeThrough.key) { var textColor = defaultStyles.color; if (color?.value is String) { @@ -212,6 +213,10 @@ class TextLine extends StatelessWidget { } }); + if (nodeStyle.containsKey(Attribute.inlineCode.key)) { + res = _merge(res, defaultStyles.inlineCode!.styleFor(lineStyle)); + } + final font = textNode.style.attributes[Attribute.font.key]; if (font != null && font.value != null) { res = res.merge(TextStyle(fontFamily: font.value)); @@ -323,6 +328,7 @@ class EditableTextLine extends RenderObjectWidget { @override RenderObject createRenderObject(BuildContext context) { + final defaultStyles = DefaultStyles.getInstance(context); return RenderEditableTextLine( line, textDirection, @@ -332,12 +338,14 @@ class EditableTextLine extends RenderObjectWidget { devicePixelRatio, _getPadding(), color, - cursorCont); + cursorCont, + defaultStyles.inlineCode!); } @override void updateRenderObject( BuildContext context, covariant RenderEditableTextLine renderObject) { + final defaultStyles = DefaultStyles.getInstance(context); renderObject ..setLine(line) ..setPadding(_getPadding()) @@ -347,7 +355,8 @@ class EditableTextLine extends RenderObjectWidget { ..setEnableInteractiveSelection(enableInteractiveSelection) ..hasFocus = hasFocus ..setDevicePixelRatio(devicePixelRatio) - ..setCursorCont(cursorCont); + ..setCursorCont(cursorCont) + ..setInlineCodeStyle(defaultStyles.inlineCode!); } EdgeInsetsGeometry _getPadding() { @@ -361,17 +370,18 @@ class EditableTextLine extends RenderObjectWidget { enum TextLineSlot { LEADING, BODY } class RenderEditableTextLine extends RenderEditableBox { + /// Creates new editable paragraph render box. RenderEditableTextLine( - this.line, - this.textDirection, - this.textSelection, - this.enableInteractiveSelection, - this.hasFocus, - this.devicePixelRatio, - this.padding, - this.color, - this.cursorCont, - ); + this.line, + this.textDirection, + this.textSelection, + this.enableInteractiveSelection, + this.hasFocus, + this.devicePixelRatio, + this.padding, + this.color, + this.cursorCont, + this.inlineCodeStyle); RenderBox? _leading; RenderContentProxyBox? _body; @@ -388,6 +398,7 @@ class RenderEditableTextLine extends RenderEditableBox { bool? _containsCursor; List? _selectedRects; late Rect _caretPrototype; + InlineCodeStyle inlineCodeStyle; final Map children = {}; Iterable get _children sync* { @@ -497,6 +508,14 @@ class RenderEditableTextLine extends RenderEditableBox { _body = _updateChild(_body, b, TextLineSlot.BODY) as RenderContentProxyBox?; } + void setInlineCodeStyle(InlineCodeStyle newStyle) { + if (inlineCodeStyle == newStyle) return; + inlineCodeStyle = newStyle; + markNeedsLayout(); + } + + // Start selection implementation + bool containsTextSelection() { return line.documentOffset <= textSelection.end && textSelection.start <= line.documentOffset + line.length - 1; @@ -870,6 +889,31 @@ class RenderEditableTextLine extends RenderEditableBox { final parentData = _body!.parentData as BoxParentData; final effectiveOffset = offset + parentData.offset; + if (inlineCodeStyle.backgroundColor != null) { + for (final item in line.children) { + if (item is! leaf.Text || + !item.style.containsKey(Attribute.inlineCode.key)) { + continue; + } + final textRange = TextSelection( + baseOffset: item.offset, extentOffset: item.offset + item.length); + final rects = _body!.getBoxesForSelection(textRange); + final paint = Paint()..color = inlineCodeStyle.backgroundColor!; + for (final box in rects) { + final rect = box.toRect().translate(0, 1).shift(effectiveOffset); + if (inlineCodeStyle.radius == null) { + final paintRect = Rect.fromLTRB( + rect.left - 2, rect.top, rect.right + 2, rect.bottom); + context.canvas.drawRect(paintRect, paint); + } else { + final paintRect = RRect.fromLTRBR(rect.left - 2, rect.top, + rect.right + 2, rect.bottom, inlineCodeStyle.radius!); + context.canvas.drawRRect(paintRect, paint); + } + } + } + } + if (hasFocus && cursorCont.show.value && containsCursor() && From b07ad1fa079b6a574132a0f300a1f8d6909f38ad Mon Sep 17 00:00:00 2001 From: X Code Date: Tue, 14 Dec 2021 23:27:41 -0800 Subject: [PATCH 125/179] Upgrade to 2.4.0 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index d9ea96ad..4607c5a3 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: 2.3.3 +version: 2.4.0 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 263ee8faced7d97156098bab592f3b00865bc24c Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 15 Dec 2021 18:52:43 -0800 Subject: [PATCH 126/179] Remove _sentRemoteValues --- ..._editor_state_text_input_client_mixin.dart | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart index c0443349..a9923d27 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart @@ -10,7 +10,6 @@ import '../editor.dart'; mixin RawEditorStateTextInputClientMixin on EditorState implements TextInputClient { - final List _sentRemoteValues = []; TextInputConnection? _textInputConnection; TextEditingValue? _lastKnownRemoteTextEditingValue; @@ -77,7 +76,6 @@ mixin RawEditorStateTextInputClientMixin on EditorState _textInputConnection!.close(); _textInputConnection = null; _lastKnownRemoteTextEditingValue = null; - _sentRemoteValues.clear(); } /// Updates remote value based on current state of [document] and @@ -105,17 +103,12 @@ mixin RawEditorStateTextInputClientMixin on EditorState return; } - final shouldRemember = value.text != _lastKnownRemoteTextEditingValue!.text; _lastKnownRemoteTextEditingValue = actualValue; _textInputConnection!.setEditingState( // Set composing to (-1, -1), otherwise an exception will be thrown if // the values are different. actualValue.copyWith(composing: const TextRange(start: -1, end: -1)), ); - if (shouldRemember) { - // Only keep track if text changed (selection changes are not relevant) - _sentRemoteValues.add(actualValue); - } } @override @@ -132,22 +125,6 @@ mixin RawEditorStateTextInputClientMixin on EditorState 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; @@ -317,6 +294,5 @@ mixin RawEditorStateTextInputClientMixin on EditorState _textInputConnection!.connectionClosedReceived(); _textInputConnection = null; _lastKnownRemoteTextEditingValue = null; - _sentRemoteValues.clear(); } } From a0f3830ad5362b9e5b2164eb2b1ec470cd845e89 Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 15 Dec 2021 21:43:16 -0800 Subject: [PATCH 127/179] Hide selection overlay for collapsed selection --- lib/src/widgets/raw_editor.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 01b3a351..ec140f25 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -530,7 +530,7 @@ class RawEditorState extends EditorState void _updateOrDisposeSelectionOverlayIfNeeded() { if (_selectionOverlay != null) { - if (_hasFocus) { + if (_hasFocus && !textEditingValue.selection.isCollapsed) { _selectionOverlay!.update(textEditingValue); } else { _selectionOverlay!.dispose(); From 048f4e5b7f83c65709abf5154c38daf85003b518 Mon Sep 17 00:00:00 2001 From: Arshak Aghakaryan Date: Thu, 16 Dec 2021 18:36:59 +0400 Subject: [PATCH 128/179] Request keyboard focus when no child is found (#531) --- lib/src/widgets/editor.dart | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 0cbffe10..a14fa082 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -599,16 +599,22 @@ class _QuillEditorSelectionGestureDetectorBuilder break; case PointerDeviceKind.touch: case PointerDeviceKind.unknown: - getRenderEditor()!.selectWordEdge(SelectionChangedCause.tap); - break; + try { + getRenderEditor()!.selectWordEdge(SelectionChangedCause.tap); + } finally { + break; + } } break; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: - getRenderEditor()!.selectPosition(SelectionChangedCause.tap); - break; + try { + getRenderEditor()!.selectPosition(SelectionChangedCause.tap); + } finally { + break; + } } } _state._requestKeyboard(); From df209367377b7a0a632ebb430b77fa700306ebe4 Mon Sep 17 00:00:00 2001 From: X Code Date: Sat, 18 Dec 2021 15:34:06 -0800 Subject: [PATCH 129/179] Desktop selection improvements Added `Shift` + `Click` support for extending selection on desktop Added automatic scrolling while selecting text with mouse --- lib/src/widgets/delegate.dart | 27 ++- lib/src/widgets/editor.dart | 163 ++++++++++++++---- .../quill_single_child_scroll_view.dart | 12 -- lib/src/widgets/raw_editor.dart | 39 +++-- lib/src/widgets/simple_viewer.dart | 27 ++- 5 files changed, 177 insertions(+), 91 deletions(-) diff --git a/lib/src/widgets/delegate.dart b/lib/src/widgets/delegate.dart index 14adb9a9..bd681e76 100644 --- a/lib/src/widgets/delegate.dart +++ b/lib/src/widgets/delegate.dart @@ -76,9 +76,8 @@ class EditorTextSelectionGestureDetectorBuilder { void onSingleLongTapStart(LongPressStartDetails details) { if (delegate.getSelectionEnabled()) { getRenderEditor()!.selectPositionAt( - details.globalPosition, - null, - SelectionChangedCause.longPress, + from: details.globalPosition, + cause: SelectionChangedCause.longPress, ); } } @@ -86,9 +85,8 @@ class EditorTextSelectionGestureDetectorBuilder { void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { if (delegate.getSelectionEnabled()) { getRenderEditor()!.selectPositionAt( - details.globalPosition, - null, - SelectionChangedCause.longPress, + from: details.globalPosition, + cause: SelectionChangedCause.longPress, ); } } @@ -109,23 +107,18 @@ class EditorTextSelectionGestureDetectorBuilder { } void onDragSelectionStart(DragStartDetails details) { - getRenderEditor()!.selectPositionAt( - details.globalPosition, - null, - SelectionChangedCause.drag, - ); + getRenderEditor()!.handleDragStart(details); } void onDragSelectionUpdate( DragStartDetails startDetails, DragUpdateDetails updateDetails) { - getRenderEditor()!.selectPositionAt( - startDetails.globalPosition, - updateDetails.globalPosition, - SelectionChangedCause.drag, - ); + getRenderEditor()!.extendSelection(updateDetails.globalPosition, + cause: SelectionChangedCause.drag); } - void onDragSelectionEnd(DragEndDetails details) {} + void onDragSelectionEnd(DragEndDetails details) { + getRenderEditor()!.handleDragEnd(details); + } Widget build(HitTestBehavior behavior, Widget child) { return EditorTextSelectionGestureDetector( diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index a14fa082..99b3500d 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -130,8 +130,13 @@ abstract class RenderAbstractEditor implements TextLayoutMetrics { /// {@macro flutter.rendering.editable.select} void selectWordEdge(SelectionChangedCause cause); - /// Select text between the global positions [from] and [to]. - void selectPositionAt(Offset from, Offset to, SelectionChangedCause cause); + /// + /// Returns the new selection. Note that the returned value may not be + /// yet reflected in the latest widget state. + /// + /// Returns null if no change occurred. + TextSelection? selectPositionAt( + {required Offset from, required SelectionChangedCause cause, Offset? to}); /// Select a word around the location of the last tap down. /// @@ -148,7 +153,7 @@ abstract class RenderAbstractEditor implements TextLayoutMetrics { /// If you have a [TextEditingController], it's generally easier to /// programmatically manipulate its `value` or `selection` directly. /// {@endtemplate} - void selectPosition(SelectionChangedCause cause); + void selectPosition({required SelectionChangedCause cause}); } String _standardizeImageUrl(String url) { @@ -475,9 +480,8 @@ class _QuillEditorSelectionGestureDetectorBuilder case TargetPlatform.iOS: case TargetPlatform.macOS: getRenderEditor()!.selectPositionAt( - details.globalPosition, - null, - SelectionChangedCause.longPress, + from: details.globalPosition, + cause: SelectionChangedCause.longPress, ); break; case TargetPlatform.android: @@ -571,6 +575,13 @@ class _QuillEditorSelectionGestureDetectorBuilder super.onTapDown(details); } + bool isShiftClick(PointerDeviceKind deviceKind) { + final pressed = RawKeyboard.instance.keysPressed; + return deviceKind == PointerDeviceKind.mouse && + (pressed.contains(LogicalKeyboardKey.shiftLeft) || + pressed.contains(LogicalKeyboardKey.shiftRight)); + } + @override void onSingleTapUp(TapUpDetails details) { if (_state.widget.onTapUp != null) { @@ -595,7 +606,17 @@ class _QuillEditorSelectionGestureDetectorBuilder case PointerDeviceKind.mouse: case PointerDeviceKind.stylus: case PointerDeviceKind.invertedStylus: - getRenderEditor()!.selectPosition(SelectionChangedCause.tap); + // Precise devices should place the cursor at a precise position. + // If `Shift` key is pressed then + // extend current selection instead. + if (isShiftClick(details.kind)) { + getRenderEditor()!.extendSelection(details.globalPosition, + cause: SelectionChangedCause.tap); + } else { + getRenderEditor()! + .selectPosition(cause: SelectionChangedCause.tap); + } + break; case PointerDeviceKind.touch: case PointerDeviceKind.unknown: @@ -611,7 +632,7 @@ class _QuillEditorSelectionGestureDetectorBuilder case TargetPlatform.linux: case TargetPlatform.windows: try { - getRenderEditor()!.selectPosition(SelectionChangedCause.tap); + getRenderEditor()!.selectPosition(cause: SelectionChangedCause.tap); } finally { break; } @@ -637,9 +658,8 @@ class _QuillEditorSelectionGestureDetectorBuilder case TargetPlatform.iOS: case TargetPlatform.macOS: getRenderEditor()!.selectPositionAt( - details.globalPosition, - null, - SelectionChangedCause.longPress, + from: details.globalPosition, + cause: SelectionChangedCause.longPress, ); break; case TargetPlatform.android: @@ -686,25 +706,35 @@ const EdgeInsets _kFloatingCursorAddedMargin = EdgeInsets.fromLTRB(4, 4, 4, 5); const EdgeInsets _kFloatingCaretSizeIncrease = EdgeInsets.symmetric(horizontal: 0.5, vertical: 1); +/// Displays a document as a vertical list of document segments (lines +/// and blocks). +/// +/// Children of [RenderEditor] must be instances of [RenderEditableBox]. class RenderEditor extends RenderEditableContainerBox with RelayoutWhenSystemFontsChangeMixin implements RenderAbstractEditor { - RenderEditor( - ViewportOffset? offset, - List? children, - TextDirection textDirection, - double scrollBottomInset, - EdgeInsetsGeometry padding, - this.document, - this.selection, - this._hasFocus, - this.onSelectionChanged, - this._startHandleLayerLink, - this._endHandleLayerLink, - EdgeInsets floatingCursorAddedMargin, - this._cursorController, - this.floatingCursorDisabled) - : super( + RenderEditor({ + required this.document, + required TextDirection textDirection, + required bool hasFocus, + required this.selection, + required LayerLink startHandleLayerLink, + required LayerLink endHandleLayerLink, + required EdgeInsetsGeometry padding, + required CursorCont cursorController, + required this.onSelectionChanged, + required double scrollBottomInset, + required this.floatingCursorDisabled, + ViewportOffset? offset, + List? children, + EdgeInsets floatingCursorAddedMargin = + const EdgeInsets.fromLTRB(4, 4, 4, 5), + }) : _hasFocus = hasFocus, + _extendSelectionOrigin = selection, + _startHandleLayerLink = startHandleLayerLink, + _endHandleLayerLink = endHandleLayerLink, + _cursorController = cursorController, + super( children, document.root, textDirection, @@ -720,6 +750,8 @@ class RenderEditor extends RenderEditableContainerBox bool _hasFocus = false; LayerLink _startHandleLayerLink; LayerLink _endHandleLayerLink; + + /// Called when the selection changes. TextSelectionChangedHandler onSelectionChanged; final ValueNotifier _selectionStartInViewport = ValueNotifier(true); @@ -800,8 +832,18 @@ class RenderEditor extends RenderEditableContainerBox } selection = t; markNeedsPaint(); + + if (!_shiftPressed && !_isDragging) { + // Only update extend selection origin if Shift key is not pressed and + // user is not dragging selection. + _extendSelectionOrigin = selection; + } } + bool get _shiftPressed => + RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftLeft) || + RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftLeft); + void setStartHandleLayerLink(LayerLink value) { if (_startHandleLayerLink == value) { return; @@ -885,11 +927,35 @@ class RenderEditor extends RenderEditableContainerBox Offset? _lastTapDownPosition; + // Used on Desktop (mouse and keyboard enabled platforms) as base offset + // for extending selection, either with combination of `Shift` + Click or + // by dragging + TextSelection? _extendSelectionOrigin; + @override void handleTapDown(TapDownDetails details) { _lastTapDownPosition = details.globalPosition; } + bool _isDragging = false; + + void handleDragStart(DragStartDetails details) { + _isDragging = true; + + final newSelection = selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.drag, + ); + + if (newSelection == null) return; + // Make sure to remember the origin for extend selection. + _extendSelectionOrigin = newSelection; + } + + void handleDragEnd(DragEndDetails details) { + _isDragging = false; + } + @override void selectWordsInRange( Offset from, @@ -926,6 +992,34 @@ class RenderEditor extends RenderEditableContainerBox onSelectionChanged(nextSelection, cause); } + /// Extends current selection to the position closest to specified offset. + void extendSelection(Offset to, {required SelectionChangedCause cause}) { + /// The below logic does not exactly match the native version because + /// we do not allow swapping of base and extent positions. + assert(_extendSelectionOrigin != null); + final position = getPositionForOffset(to); + + if (position.offset < _extendSelectionOrigin!.baseOffset) { + _handleSelectionChange( + TextSelection( + baseOffset: position.offset, + extentOffset: _extendSelectionOrigin!.extentOffset, + affinity: selection.affinity, + ), + cause, + ); + } else if (position.offset > _extendSelectionOrigin!.extentOffset) { + _handleSelectionChange( + TextSelection( + baseOffset: _extendSelectionOrigin!.baseOffset, + extentOffset: position.offset, + affinity: selection.affinity, + ), + cause, + ); + } + } + @override void selectWordEdge(SelectionChangedCause cause) { assert(_lastTapDownPosition != null); @@ -956,11 +1050,11 @@ class RenderEditor extends RenderEditableContainerBox } @override - void selectPositionAt( - Offset from, + TextSelection? selectPositionAt({ + required Offset from, + required SelectionChangedCause cause, Offset? to, - SelectionChangedCause cause, - ) { + }) { final fromPosition = getPositionForOffset(from); final toPosition = to == null ? null : getPositionForOffset(to); @@ -976,7 +1070,10 @@ class RenderEditor extends RenderEditableContainerBox extentOffset: extentOffset, affinity: fromPosition.affinity, ); + + // Call [onSelectionChanged] only when the selection actually changed. _handleSelectionChange(newSelection, cause); + return newSelection; } @override @@ -985,8 +1082,8 @@ class RenderEditor extends RenderEditableContainerBox } @override - void selectPosition(SelectionChangedCause cause) { - selectPositionAt(_lastTapDownPosition!, null, cause); + void selectPosition({required SelectionChangedCause cause}) { + selectPositionAt(from: _lastTapDownPosition!, cause: cause); } @override diff --git a/lib/src/widgets/quill_single_child_scroll_view.dart b/lib/src/widgets/quill_single_child_scroll_view.dart index 77b127b1..40971b06 100644 --- a/lib/src/widgets/quill_single_child_scroll_view.dart +++ b/lib/src/widgets/quill_single_child_scroll_view.dart @@ -140,18 +140,6 @@ class _RenderSingleChildViewport extends RenderBox if (child.parentData is! ParentData) child.parentData = ParentData(); } - @override - void attach(PipelineOwner owner) { - super.attach(owner); - _offset.addListener(_hasScrolled); - } - - @override - void detach() { - _offset.removeListener(_hasScrolled); - super.detach(); - } - @override bool get isRepaintBoundary => true; diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index ec140f25..0caba63e 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -228,13 +228,26 @@ class RawEditorState extends EditorState void _handleSelectionChanged( TextSelection selection, SelectionChangedCause cause) { + final oldSelection = widget.controller.selection; widget.controller.updateSelection(selection, ChangeSource.LOCAL); _selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); if (!_keyboardVisible) { + // This will show the keyboard for all selection changes on the + // editor, not just changes triggered by user gestures. requestKeyboard(); } + + if (cause == SelectionChangedCause.drag) { + // When user updates the selection while dragging make sure to + // bring the updated position (base or extent) into view. + if (oldSelection.baseOffset != selection.baseOffset) { + bringIntoView(selection.base); + } else if (oldSelection.extentOffset != selection.extentOffset) { + bringIntoView(selection.extent); + } + } } /// Updates the checkbox positioned at [offset] in document @@ -838,20 +851,18 @@ class _Editor extends MultiChildRenderObjectWidget { @override RenderEditor createRenderObject(BuildContext context) { return RenderEditor( - offset, - null, - textDirection, - scrollBottomInset, - padding, - document, - selection, - hasFocus, - onSelectionChanged, - startHandleLayerLink, - endHandleLayerLink, - const EdgeInsets.fromLTRB(4, 4, 4, 5), - cursorController, - floatingCursorDisabled); + offset: offset, + document: document, + textDirection: textDirection, + hasFocus: hasFocus, + selection: selection, + startHandleLayerLink: startHandleLayerLink, + endHandleLayerLink: endHandleLayerLink, + onSelectionChanged: onSelectionChanged, + cursorController: cursorController, + padding: padding, + scrollBottomInset: scrollBottomInset, + floatingCursorDisabled: floatingCursorDisabled); } @override diff --git a/lib/src/widgets/simple_viewer.dart b/lib/src/widgets/simple_viewer.dart index ab749063..c48173af 100644 --- a/lib/src/widgets/simple_viewer.dart +++ b/lib/src/widgets/simple_viewer.dart @@ -337,21 +337,18 @@ class _SimpleViewer extends MultiChildRenderObjectWidget { @override RenderEditor createRenderObject(BuildContext context) { return RenderEditor( - offset, - null, - textDirection, - scrollBottomInset, - padding, - document, - const TextSelection(baseOffset: 0, extentOffset: 0), - false, - // hasFocus, - onSelectionChanged, - startHandleLayerLink, - endHandleLayerLink, - const EdgeInsets.fromLTRB(4, 4, 4, 5), - cursorController, - floatingCursorDisabled); + offset: offset, + document: document, + textDirection: textDirection, + hasFocus: false, + selection: const TextSelection(baseOffset: 0, extentOffset: 0), + startHandleLayerLink: startHandleLayerLink, + endHandleLayerLink: endHandleLayerLink, + onSelectionChanged: onSelectionChanged, + cursorController: cursorController, + padding: padding, + scrollBottomInset: scrollBottomInset, + floatingCursorDisabled: floatingCursorDisabled); } @override From 0d145f32233e8c24f4bb22e5c9e059b5b71badc8 Mon Sep 17 00:00:00 2001 From: X Code Date: Sat, 18 Dec 2021 15:35:54 -0800 Subject: [PATCH 130/179] Upgrade to 2.4.1 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a4bd971..e7ffa78a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.4.1] +* Desktop selection improvements. + ## [2.4.0] * Improve inline code style. diff --git a/pubspec.yaml b/pubspec.yaml index 4607c5a3..55544d6d 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: 2.4.0 +version: 2.4.1 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 325bb3527cc2b8d86096985cab1091ed7ac23320 Mon Sep 17 00:00:00 2001 From: X Code Date: Sat, 18 Dec 2021 16:45:34 -0800 Subject: [PATCH 131/179] Add comments --- lib/src/models/rules/format.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/src/models/rules/format.dart b/lib/src/models/rules/format.dart index 62a41317..1a2f2e5a 100644 --- a/lib/src/models/rules/format.dart +++ b/lib/src/models/rules/format.dart @@ -16,6 +16,8 @@ abstract class FormatRule extends Rule { } } +/// Produces Delta with line-level attributes applied strictly to +/// newline characters. class ResolveLineFormatRule extends FormatRule { const ResolveLineFormatRule(); @@ -26,6 +28,8 @@ class ResolveLineFormatRule extends FormatRule { return null; } + // Apply line styles to all newline characters within range of this + // retain operation. var delta = Delta()..retain(index); final itr = DeltaIterator(document)..skip(index); Operation op; @@ -89,6 +93,7 @@ class ResolveLineFormatRule extends FormatRule { } } +/// Allows updating link format with collapsed selection. class FormatLinkAtCaretPositionRule extends FormatRule { const FormatLinkAtCaretPositionRule(); @@ -121,6 +126,8 @@ class FormatLinkAtCaretPositionRule extends FormatRule { } } +/// Produces Delta with inline-level attributes applied too all characters +/// except newlines. class ResolveInlineFormatRule extends FormatRule { const ResolveInlineFormatRule(); From 5c12b11777710342b8550bfe736d3c5851187799 Mon Sep 17 00:00:00 2001 From: X Code Date: Sat, 18 Dec 2021 17:38:43 -0800 Subject: [PATCH 132/179] Refactor out _getRemovedBlocks method --- lib/src/models/rules/format.dart | 39 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/lib/src/models/rules/format.dart b/lib/src/models/rules/format.dart index 1a2f2e5a..3c23b15f 100644 --- a/lib/src/models/rules/format.dart +++ b/lib/src/models/rules/format.dart @@ -43,16 +43,7 @@ class ResolveLineFormatRule extends FormatRule { final tmp = Delta(); var offset = 0; - // Enforce Block Format exclusivity by rule - final removedBlocks = Attribute.exclusiveBlockKeys.contains(attribute.key) - ? op.attributes?.keys - .where((key) => - Attribute.exclusiveBlockKeys.contains(key) && - attribute.key != key && - attribute.value != null) - .map((key) => MapEntry(key, null)) ?? - [] - : >[]; + final removedBlocks = _getRemovedBlocks(attribute, op); for (var lineBreak = text.indexOf('\n'); lineBreak >= 0; @@ -74,16 +65,8 @@ class ResolveLineFormatRule extends FormatRule { delta.retain(op.length!); continue; } - // Enforce Block Format exclusivity by rule - final removedBlocks = Attribute.exclusiveBlockKeys.contains(attribute.key) - ? op.attributes?.keys - .where((key) => - Attribute.exclusiveBlockKeys.contains(key) && - attribute.key != key && - attribute.value != null) - .map((key) => MapEntry(key, null)) ?? - [] - : >[]; + + final removedBlocks = _getRemovedBlocks(attribute, op); delta ..retain(lineBreak) ..retain(1, attribute.toJson()..addEntries(removedBlocks)); @@ -91,6 +74,22 @@ class ResolveLineFormatRule extends FormatRule { } return delta; } + + Iterable> _getRemovedBlocks( + Attribute attribute, Operation op) { + // Enforce Block Format exclusivity by rule + if (!Attribute.exclusiveBlockKeys.contains(attribute.key)) { + return >[]; + } + + return op.attributes?.keys + .where((key) => + Attribute.exclusiveBlockKeys.contains(key) && + attribute.key != key && + attribute.value != null) + .map((key) => MapEntry(key, null)) ?? + []; + } } /// Allows updating link format with collapsed selection. From b8f016e831a5befaca582ef6d952299fe344b92e Mon Sep 17 00:00:00 2001 From: X Code Date: Sat, 18 Dec 2021 17:47:19 -0800 Subject: [PATCH 133/179] Refactor code of ResolveLineFormatRule --- lib/src/models/rules/format.dart | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/src/models/rules/format.dart b/lib/src/models/rules/format.dart index 3c23b15f..d757e92d 100644 --- a/lib/src/models/rules/format.dart +++ b/lib/src/models/rules/format.dart @@ -30,13 +30,14 @@ class ResolveLineFormatRule extends FormatRule { // Apply line styles to all newline characters within range of this // retain operation. - var delta = Delta()..retain(index); + var result = Delta()..retain(index); final itr = DeltaIterator(document)..skip(index); Operation op; for (var cur = 0; cur < len! && itr.hasNext; cur += op.length!) { op = itr.next(len - cur); - if (op.data is! String || !(op.data as String).contains('\n')) { - delta.retain(op.length!); + final opText = op.data is String ? op.data as String : ''; + if (!opText.contains('\n')) { + result.retain(op.length!); continue; } final text = op.data as String; @@ -54,7 +55,7 @@ class ResolveLineFormatRule extends FormatRule { offset = lineBreak + 1; } tmp.retain(text.length - offset); - delta = delta.concat(tmp); + result = result.concat(tmp); } while (itr.hasNext) { @@ -62,17 +63,17 @@ class ResolveLineFormatRule extends FormatRule { final text = op.data is String ? (op.data as String?)! : ''; final lineBreak = text.indexOf('\n'); if (lineBreak < 0) { - delta.retain(op.length!); + result.retain(op.length!); continue; } final removedBlocks = _getRemovedBlocks(attribute, op); - delta + result ..retain(lineBreak) ..retain(1, attribute.toJson()..addEntries(removedBlocks)); break; } - return delta; + return result; } Iterable> _getRemovedBlocks( From 1aeb7028ae771cd771be55de6d46ceff39ce8402 Mon Sep 17 00:00:00 2001 From: X Code Date: Sat, 18 Dec 2021 18:02:04 -0800 Subject: [PATCH 134/179] Refactor code of ResolveLineFormatRule - no logic change --- lib/src/models/rules/format.dart | 57 ++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/lib/src/models/rules/format.dart b/lib/src/models/rules/format.dart index d757e92d..634808e4 100644 --- a/lib/src/models/rules/format.dart +++ b/lib/src/models/rules/format.dart @@ -40,42 +40,51 @@ class ResolveLineFormatRule extends FormatRule { result.retain(op.length!); continue; } - final text = op.data as String; - final tmp = Delta(); - var offset = 0; - - final removedBlocks = _getRemovedBlocks(attribute, op); - - for (var lineBreak = text.indexOf('\n'); - lineBreak >= 0; - lineBreak = text.indexOf('\n', offset)) { - tmp - ..retain(lineBreak - offset) - ..retain(1, attribute.toJson()..addEntries(removedBlocks)); - offset = lineBreak + 1; - } - tmp.retain(text.length - offset); - result = result.concat(tmp); - } + final delta = _applyAttribute(opText, op, attribute); + result = result.concat(delta); + } + // And include extra newline after retain while (itr.hasNext) { op = itr.next(); - final text = op.data is String ? (op.data as String?)! : ''; - final lineBreak = text.indexOf('\n'); - if (lineBreak < 0) { + final opText = op.data is String ? op.data as String : ''; + final lf = opText.indexOf('\n'); + if (lf < 0) { result.retain(op.length!); continue; } - final removedBlocks = _getRemovedBlocks(attribute, op); - result - ..retain(lineBreak) - ..retain(1, attribute.toJson()..addEntries(removedBlocks)); + final delta = _applyAttribute(opText, op, attribute, firstOnly: true); + result = result.concat(delta); break; } return result; } + Delta _applyAttribute(String text, Operation op, Attribute attribute, + {bool firstOnly = false}) { + final result = Delta(); + var offset = 0; + var lf = text.indexOf('\n'); + final removedBlocks = _getRemovedBlocks(attribute, op); + while (lf >= 0) { + final actualStyle = attribute.toJson()..addEntries(removedBlocks); + result + ..retain(lf - offset) + ..retain(1, actualStyle); + + if (firstOnly) { + return result; + } + + offset = lf + 1; + lf = text.indexOf('\n', offset); + } + // Retain any remaining characters in text + result.retain(text.length - offset); + return result; + } + Iterable> _getRemovedBlocks( Attribute attribute, Operation op) { // Enforce Block Format exclusivity by rule From 25980df3fbc016666903371496ffca0e415071ca Mon Sep 17 00:00:00 2001 From: X Code Date: Sat, 18 Dec 2021 18:23:34 -0800 Subject: [PATCH 135/179] PreserveBlockStyleOnInsertRule supports checked list --- lib/src/models/rules/insert.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart index 67522d38..7408b20c 100644 --- a/lib/src/models/rules/insert.dart +++ b/lib/src/models/rules/insert.dart @@ -87,12 +87,17 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { return null; } - Map? resetStyle; + final resetStyle = {}; // If current line had heading style applied to it we'll need to move this // style to the newly inserted line before it and reset style of the // original line. if (lineStyle.containsKey(Attribute.header.key)) { - resetStyle = Attribute.header.toJson(); + resetStyle.addAll(Attribute.header.toJson()); + } + + // Similarly for the checked style + if (lineStyle.attributes[Attribute.list.key] == Attribute.checked) { + resetStyle.addAll(Attribute.checked.toJson()); } // Go over each inserted line and ensure block style is applied. @@ -113,7 +118,7 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { } // Reset style of the original newline character if needed. - if (resetStyle != null) { + if (resetStyle.isNotEmpty) { delta ..retain(nextNewLine.item2!) ..retain((nextNewLine.item1!.data as String).indexOf('\n')) From 567315a5a29844d37c99b64dae343c285337ee52 Mon Sep 17 00:00:00 2001 From: X Code Date: Sat, 18 Dec 2021 18:27:45 -0800 Subject: [PATCH 136/179] PreserveBlockStyleOnInsertRule supports unchecked list --- lib/src/models/rules/insert.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart index 7408b20c..487696c0 100644 --- a/lib/src/models/rules/insert.dart +++ b/lib/src/models/rules/insert.dart @@ -100,6 +100,11 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { resetStyle.addAll(Attribute.checked.toJson()); } + // Similarly for the unchecked style + if (lineStyle.attributes[Attribute.list.key] == Attribute.unchecked) { + resetStyle.addAll(Attribute.unchecked.toJson()); + } + // Go over each inserted line and ensure block style is applied. final lines = data.split('\n'); final delta = Delta()..retain(index + (len ?? 0)); From aa213a5e9e684f9e975720b2b220f82daa72905a Mon Sep 17 00:00:00 2001 From: X Code Date: Sat, 18 Dec 2021 19:00:27 -0800 Subject: [PATCH 137/179] Checked list is already considered block --- lib/src/models/rules/insert.dart | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart index 487696c0..a8acb0e9 100644 --- a/lib/src/models/rules/insert.dart +++ b/lib/src/models/rules/insert.dart @@ -95,16 +95,6 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { resetStyle.addAll(Attribute.header.toJson()); } - // Similarly for the checked style - if (lineStyle.attributes[Attribute.list.key] == Attribute.checked) { - resetStyle.addAll(Attribute.checked.toJson()); - } - - // Similarly for the unchecked style - if (lineStyle.attributes[Attribute.list.key] == Attribute.unchecked) { - resetStyle.addAll(Attribute.unchecked.toJson()); - } - // Go over each inserted line and ensure block style is applied. final lines = data.split('\n'); final delta = Delta()..retain(index + (len ?? 0)); From d2444a2af0a04f1e5feb685f25fa6a3b91605753 Mon Sep 17 00:00:00 2001 From: X Code Date: Sun, 19 Dec 2021 09:02:34 -0800 Subject: [PATCH 138/179] Fix checkbox functionality (#541) * Fix checkbox functionality * Remove class QuillCheckbox --- lib/src/widgets/raw_editor.dart | 7 +- lib/src/widgets/style_widgets/checkbox.dart | 60 -------------- .../widgets/style_widgets/checkbox_point.dart | 78 +++++++++++++++++++ .../widgets/style_widgets/style_widgets.dart | 2 +- lib/src/widgets/text_block.dart | 25 +++--- lib/src/widgets/text_line.dart | 11 +++ 6 files changed, 102 insertions(+), 81 deletions(-) delete mode 100644 lib/src/widgets/style_widgets/checkbox.dart create mode 100644 lib/src/widgets/style_widgets/checkbox_point.dart diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 0caba63e..ce9540ab 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -254,11 +254,8 @@ class RawEditorState extends EditorState /// 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); - } + widget.controller.formatText( + offset, 0, value ? Attribute.checked : Attribute.unchecked); } } diff --git a/lib/src/widgets/style_widgets/checkbox.dart b/lib/src/widgets/style_widgets/checkbox.dart deleted file mode 100644 index 64f36120..00000000 --- a/lib/src/widgets/style_widgets/checkbox.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; - -class QuillCheckbox extends StatelessWidget { - const QuillCheckbox({ - Key? key, - this.style, - this.width, - this.isChecked = false, - this.offset, - this.onTap, - this.uiBuilder, - }) : super(key: key); - final TextStyle? style; - final double? width; - final bool isChecked; - final int? offset; - final Function(int, bool)? onTap; - final QuillCheckboxBuilder? uiBuilder; - - void _onCheckboxClicked(bool? newValue) { - if (onTap != null && newValue != null && offset != null) { - onTap!(offset!, newValue); - } - } - - @override - Widget build(BuildContext context) { - Widget child; - if (uiBuilder != null) { - child = uiBuilder!.build( - context: context, - isChecked: isChecked, - onChanged: _onCheckboxClicked, - ); - } else { - child = Container( - alignment: AlignmentDirectional.topEnd, - width: width, - padding: const EdgeInsetsDirectional.only(end: 13), - child: GestureDetector( - onLongPress: () => _onCheckboxClicked(!isChecked), - child: Checkbox( - value: isChecked, - onChanged: _onCheckboxClicked, - ), - ), - ); - } - - return child; - } -} - -abstract class QuillCheckboxBuilder { - Widget build({ - required BuildContext context, - required bool isChecked, - required void Function(bool?) onChanged, - }); -} diff --git a/lib/src/widgets/style_widgets/checkbox_point.dart b/lib/src/widgets/style_widgets/checkbox_point.dart new file mode 100644 index 00000000..7f4bfe88 --- /dev/null +++ b/lib/src/widgets/style_widgets/checkbox_point.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; + +class CheckboxPoint extends StatefulWidget { + const CheckboxPoint({ + required this.size, + required this.value, + required this.enabled, + required this.onChanged, + this.uiBuilder, + Key? key, + }) : super(key: key); + + final double size; + final bool value; + final bool enabled; + final ValueChanged onChanged; + final QuillCheckboxBuilder? uiBuilder; + + @override + _CheckboxPointState createState() => _CheckboxPointState(); +} + +class _CheckboxPointState extends State { + @override + Widget build(BuildContext context) { + if (widget.uiBuilder != null) { + return widget.uiBuilder!.build( + context: context, + isChecked: widget.value, + onChanged: widget.onChanged, + ); + } + final theme = Theme.of(context); + final fillColor = widget.value + ? (widget.enabled + ? theme.colorScheme.primary + : theme.colorScheme.onSurface.withOpacity(0.5)) + : theme.colorScheme.surface; + final borderColor = widget.value + ? (widget.enabled + ? theme.colorScheme.primary + : theme.colorScheme.onSurface.withOpacity(0)) + : (widget.enabled + ? theme.colorScheme.onSurface.withOpacity(0.5) + : theme.colorScheme.onSurface.withOpacity(0.3)); + return Center( + child: SizedBox( + width: widget.size, + height: widget.size, + child: Material( + color: fillColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: borderColor, + ), + borderRadius: BorderRadius.circular(2), + ), + child: InkWell( + onTap: + widget.enabled ? () => widget.onChanged(!widget.value) : null, + child: widget.value + ? Icon(Icons.check, + size: widget.size, color: theme.colorScheme.onPrimary) + : null, + ), + ), + ), + ); + } +} + +abstract class QuillCheckboxBuilder { + Widget build({ + required BuildContext context, + required bool isChecked, + required ValueChanged onChanged, + }); +} diff --git a/lib/src/widgets/style_widgets/style_widgets.dart b/lib/src/widgets/style_widgets/style_widgets.dart index 2a252d43..c28606bc 100644 --- a/lib/src/widgets/style_widgets/style_widgets.dart +++ b/lib/src/widgets/style_widgets/style_widgets.dart @@ -1,3 +1,3 @@ export 'bullet_point.dart'; -export 'checkbox.dart'; +export 'checkbox_point.dart'; export 'number_point.dart'; diff --git a/lib/src/widgets/text_block.dart b/lib/src/widgets/text_block.dart index 33b1c351..fcfaa195 100644 --- a/lib/src/widgets/text_block.dart +++ b/lib/src/widgets/text_block.dart @@ -168,25 +168,20 @@ class EditableTextBlock extends StatelessWidget { } if (attrs[Attribute.list.key] == Attribute.checked) { - return QuillCheckbox( - key: UniqueKey(), - style: defaultStyles!.leading!.style, - width: 32, - isChecked: true, - offset: block.offset + line.offset, - onTap: onCheckboxTap, - uiBuilder: defaultStyles.lists!.checkboxUIBuilder, + return CheckboxPoint( + size: 14, + value: true, + enabled: !readOnly, + onChanged: (checked) => onCheckboxTap(line.documentOffset, checked), ); } if (attrs[Attribute.list.key] == Attribute.unchecked) { - return QuillCheckbox( - key: UniqueKey(), - style: defaultStyles!.leading!.style, - width: 32, - offset: block.offset + line.offset, - onTap: onCheckboxTap, - uiBuilder: defaultStyles.lists!.checkboxUIBuilder, + return CheckboxPoint( + size: 14, + value: false, + enabled: !readOnly, + onChanged: (checked) => onCheckboxTap(line.documentOffset, checked), ); } diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index d75daffd..a9e2dbc5 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -967,6 +967,17 @@ class RenderEditableTextLine extends RenderEditableBox { @override bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + if (_leading != null) { + final childParentData = _leading!.parentData as BoxParentData; + final isHit = result.addWithPaintOffset( + offset: childParentData.offset, + position: position, + hitTest: (result, transformed) { + assert(transformed == position - childParentData.offset); + return _leading!.hitTest(result, position: transformed); + }); + if (isHit) return true; + } if (_body == null) return false; final parentData = _body!.parentData as BoxParentData; return result.addWithPaintOffset( From 00f003ef8df01e707be42284c001278b125a86af Mon Sep 17 00:00:00 2001 From: X Code Date: Sun, 19 Dec 2021 09:04:26 -0800 Subject: [PATCH 139/179] Upgrade to 2.5.0 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7ffa78a..e788bef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.5.0] +* Update checkbox list. + ## [2.4.1] * Desktop selection improvements. diff --git a/pubspec.yaml b/pubspec.yaml index 55544d6d..cf78dc4b 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: 2.4.1 +version: 2.5.0 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From ba9875be0c58642a3aca63c06a4e5e2ce5389478 Mon Sep 17 00:00:00 2001 From: X Code Date: Sun, 19 Dec 2021 09:45:56 -0800 Subject: [PATCH 140/179] Add comments --- lib/src/models/rules/delete.dart | 8 ++++++++ lib/src/models/rules/format.dart | 1 + lib/src/models/rules/insert.dart | 15 +++++++++++++++ lib/src/models/rules/rule.dart | 2 ++ 4 files changed, 26 insertions(+) diff --git a/lib/src/models/rules/delete.dart b/lib/src/models/rules/delete.dart index 91e884d9..caf8cf6a 100644 --- a/lib/src/models/rules/delete.dart +++ b/lib/src/models/rules/delete.dart @@ -2,6 +2,7 @@ import '../documents/attribute.dart'; import '../quill_delta.dart'; import 'rule.dart'; +/// A heuristic rule for delete operations. abstract class DeleteRule extends Rule { const DeleteRule(); @@ -46,6 +47,12 @@ class CatchAllDeleteRule extends DeleteRule { } } +/// Preserves line format when user deletes the line's newline character +/// effectively merging it with the next line. +/// +/// This rule makes sure to apply all style attributes of deleted newline +/// to the next available newline, which may reset any style attributes +/// already present there. class PreserveLineStyleOnMergeRule extends DeleteRule { const PreserveLineStyleOnMergeRule(); @@ -101,6 +108,7 @@ class PreserveLineStyleOnMergeRule extends DeleteRule { } } +/// Prevents user from merging a line containing an embed with other lines. class EnsureEmbedLineRule extends DeleteRule { const EnsureEmbedLineRule(); diff --git a/lib/src/models/rules/format.dart b/lib/src/models/rules/format.dart index 634808e4..e1833b7c 100644 --- a/lib/src/models/rules/format.dart +++ b/lib/src/models/rules/format.dart @@ -2,6 +2,7 @@ import '../documents/attribute.dart'; import '../quill_delta.dart'; import 'rule.dart'; +/// A heuristic rule for format (retain) operations. abstract class FormatRule extends Rule { const FormatRule(); diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart index a8acb0e9..57b7117f 100644 --- a/lib/src/models/rules/insert.dart +++ b/lib/src/models/rules/insert.dart @@ -5,6 +5,7 @@ import '../documents/style.dart'; import '../quill_delta.dart'; import 'rule.dart'; +/// A heuristic rule for insert operations. abstract class InsertRule extends Rule { const InsertRule(); @@ -18,6 +19,10 @@ abstract class InsertRule extends Rule { } } +/// Preserves line format when user splits the line into two. +/// +/// This rule ignores scenarios when the line is split on its edge, meaning +/// a newline is inserted at the beginning or the end of a line. class PreserveLineStyleOnSplitRule extends InsertRule { const PreserveLineStyleOnSplitRule(); @@ -198,6 +203,11 @@ class AutoExitBlockRule extends InsertRule { } } +/// Resets format for a newly inserted line when insert occurred at the end +/// of a line (right before a newline). +/// +/// This handles scenarios when a new line is added when at the end of a +/// heading line. The newly added line should be a regular paragraph. class ResetLineFormatOnNewLineRule extends InsertRule { const ResetLineFormatOnNewLineRule(); @@ -227,6 +237,7 @@ class ResetLineFormatOnNewLineRule extends InsertRule { } } +/// Handles all format operations which manipulate embeds. class InsertEmbedsRule extends InsertRule { const InsertEmbedsRule(); @@ -275,6 +286,8 @@ class InsertEmbedsRule extends InsertRule { } } +/// Applies link format to text segment (which looks like a link) when user +/// inserts space character after it. class AutoFormatLinksRule extends InsertRule { const AutoFormatLinksRule(); @@ -314,6 +327,7 @@ class AutoFormatLinksRule extends InsertRule { } } +/// Preserves inline styles when user inserts text inside formatted segment. class PreserveInlineStylesRule extends InsertRule { const PreserveInlineStylesRule(); @@ -359,6 +373,7 @@ class PreserveInlineStylesRule extends InsertRule { } } +/// Fallback rule which simply inserts text as-is without any special handling. class CatchAllInsertRule extends InsertRule { const CatchAllInsertRule(); diff --git a/lib/src/models/rules/rule.dart b/lib/src/models/rules/rule.dart index c9a0f911..f01eb438 100644 --- a/lib/src/models/rules/rule.dart +++ b/lib/src/models/rules/rule.dart @@ -19,6 +19,8 @@ abstract class Rule { void validateArgs(int? len, Object? data, Attribute? attribute); + /// Applies heuristic rule to an operation on a [document] and returns + /// resulting [Delta]. Delta? applyRule(Delta document, int index, {int? len, Object? data, Attribute? attribute}); From 30e6e0cb3b077ae2f77104999062cf586ea95872 Mon Sep 17 00:00:00 2001 From: X Code Date: Sun, 19 Dec 2021 09:54:50 -0800 Subject: [PATCH 141/179] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index aea1bf50..2a391da0 100644 --- a/README.md +++ b/README.md @@ -87,14 +87,14 @@ The `QuillToolbar` class lets you customise which formatting options are availab ## Web -For web development, use `flutter config --enable-web` for flutter and use [ReactQuill] for React. +For web development, use `flutter config --enable-web` for flutter or use [ReactQuill] for React. It is required to provide `EmbedBuilder`, e.g. [defaultEmbedBuilderWeb](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/universal_ui/universal_ui.dart#L28). -Also it is required to provide `webImagePickImpl`, e.g. [Sample Page](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart#L212). +Also it is required to provide `webImagePickImpl`, e.g. [Sample Page](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart#L218). ## Desktop -It is required to provide `filePickImpl` for toolbar image button, e.g. [Sample Page](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart#L192). +It is required to provide `filePickImpl` for toolbar image button, e.g. [Sample Page](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart#L198). ## Custom Size Image for Mobile From 5a98b857a148957295c204bef0a3986f3e99fb19 Mon Sep 17 00:00:00 2001 From: X Code Date: Sun, 19 Dec 2021 10:01:04 -0800 Subject: [PATCH 142/179] [2.5.1] Bug fix for Desktop `Shift` + `Click` support --- CHANGELOG.md | 3 +++ lib/src/widgets/editor.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e788bef8..72e0794f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.5.1] +* Bug fix for Desktop `Shift` + `Click` support. + ## [2.5.0] * Update checkbox list. diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 99b3500d..a6fb480d 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -842,7 +842,7 @@ class RenderEditor extends RenderEditableContainerBox bool get _shiftPressed => RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftLeft) || - RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftLeft); + RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftRight); void setStartHandleLayerLink(LayerLink value) { if (_startHandleLayerLink == value) { diff --git a/pubspec.yaml b/pubspec.yaml index cf78dc4b..b3200073 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: 2.5.0 +version: 2.5.1 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 864c9bba449202dacd9c71f511a7b76c98cc6dd5 Mon Sep 17 00:00:00 2001 From: li3317 Date: Wed, 22 Dec 2021 23:24:43 -0500 Subject: [PATCH 143/179] fix paste --- ...w_editor_state_selection_delegate_mixin.dart | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) 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 36a5b5b2..2b51ef05 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 @@ -19,8 +19,21 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState final oldText = widget.controller.document.toPlainText(); final newText = value.text; final diff = getDiff(oldText, newText, cursorPosition); - widget.controller.replaceText( - diff.start, diff.deleted.length, diff.inserted, value.selection); + var data = diff.inserted; + if (diff.inserted.codeUnits.contains(65532)) { + final sb = StringBuffer(); + + for (var i = 0; i < data.length; i++) { + if (data.codeUnitAt(i) == 65532) { + continue; + } + sb.write(data[i]); + } + data = sb.toString(); + } + + widget.controller + .replaceText(diff.start, diff.deleted.length, data, value.selection); } @override From 373193ac3ccd8acebdde1bb137d3282f68abca9b Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 22 Dec 2021 21:19:44 -0800 Subject: [PATCH 144/179] Upgrade to 2.5.2: Skip image when pasting --- CHANGELOG.md | 3 ++ lib/src/widgets/raw_editor.dart | 54 ------------------- ...editor_state_selection_delegate_mixin.dart | 33 +++++++----- pubspec.yaml | 2 +- 4 files changed, 24 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72e0794f..dfc6b171 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.5.2] +* Skip image when pasting. + ## [2.5.1] * Bug fix for Desktop `Shift` + `Click` support. diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index ce9540ab..e8cf766d 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -661,60 +661,6 @@ class RawEditorState extends EditorState getRenderEditor()!.debugAssertLayoutUpToDate(); } - // set editing value from clipboard for mobile - Future _setEditingValue(TextEditingValue value) async { - if (await _isItCut(value)) { - widget.controller.replaceText( - textEditingValue.selection.start, - textEditingValue.text.length - value.text.length, - '', - value.selection, - ); - } else { - final value = textEditingValue; - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data != null) { - final length = - textEditingValue.selection.end - textEditingValue.selection.start; - var str = data.text!; - final codes = data.text!.codeUnits; - // For clip from editor, it may contain image, a.k.a 65532. - // For clip from browser, image is directly ignore. - // Here we skip image when pasting. - if (codes.contains(65532)) { - final sb = StringBuffer(); - for (var i = 0; i < str.length; i++) { - if (str.codeUnitAt(i) == 65532) { - continue; - } - sb.write(str[i]); - } - str = sb.toString(); - } - widget.controller.replaceText( - value.selection.start, - length, - str, - value.selection, - ); - // move cursor to the end of pasted text selection - widget.controller.updateSelection( - TextSelection.collapsed( - offset: value.selection.start + data.text!.length), - ChangeSource.LOCAL); - } - } - } - - Future _isItCut(TextEditingValue value) async { - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data == null) { - return false; - } - return textEditingValue.text.length - value.text.length == - data.text!.length; - } - @override bool showToolbar() { // Web is using native dom elements to enable clipboard functionality of the 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 2b51ef05..51d27769 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 @@ -19,21 +19,28 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState final oldText = widget.controller.document.toPlainText(); final newText = value.text; final diff = getDiff(oldText, newText, cursorPosition); - var data = diff.inserted; - if (diff.inserted.codeUnits.contains(65532)) { - final sb = StringBuffer(); - - for (var i = 0; i < data.length; i++) { - if (data.codeUnitAt(i) == 65532) { - continue; - } - sb.write(data[i]); - } - data = sb.toString(); + final insertedText = _adjustInsertedText(diff.inserted); + + widget.controller.replaceText( + diff.start, diff.deleted.length, insertedText, value.selection); + } + + String _adjustInsertedText(String text) { + // For clip from editor, it may contain image, a.k.a 65532. + // For clip from browser, image is directly ignore. + // Here we skip image when pasting. + if (!text.codeUnits.contains(65532)) { + return text; } - widget.controller - .replaceText(diff.start, diff.deleted.length, data, value.selection); + final sb = StringBuffer(); + for (var i = 0; i < text.length; i++) { + if (text.codeUnitAt(i) == 65532) { + continue; + } + sb.write(text[i]); + } + return sb.toString(); } @override diff --git a/pubspec.yaml b/pubspec.yaml index b3200073..08930761 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: 2.5.1 +version: 2.5.2 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From ba3b3cb55a11cba8662365f7df459e1a638ceb32 Mon Sep 17 00:00:00 2001 From: X Code Date: Thu, 23 Dec 2021 17:11:22 -0800 Subject: [PATCH 145/179] Format code --- .../raw_editor/raw_editor_state_selection_delegate_mixin.dart | 2 +- lib/src/widgets/style_widgets/number_point.dart | 1 + lib/src/widgets/youtube_video_app.dart | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) 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 51d27769..8c8e64c9 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 @@ -2,8 +2,8 @@ import 'dart:math' as math; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; -import '../../../utils/diff_delta.dart'; +import '../../../utils/diff_delta.dart'; import '../editor.dart'; mixin RawEditorStateSelectionDelegateMixin on EditorState diff --git a/lib/src/widgets/style_widgets/number_point.dart b/lib/src/widgets/style_widgets/number_point.dart index 8fec668c..f1ffddf1 100644 --- a/lib/src/widgets/style_widgets/number_point.dart +++ b/lib/src/widgets/style_widgets/number_point.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; + import '../../models/documents/attribute.dart'; import '../text_block.dart'; diff --git a/lib/src/widgets/youtube_video_app.dart b/lib/src/widgets/youtube_video_app.dart index eae3cb51..8f52f3a7 100644 --- a/lib/src/widgets/youtube_video_app.dart +++ b/lib/src/widgets/youtube_video_app.dart @@ -2,6 +2,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:youtube_player_flutter/youtube_player_flutter.dart'; + import '../../flutter_quill.dart'; class YoutubeVideoApp extends StatefulWidget { From b3f277edad3ad9e8d95e1f07d3d301f66fe8c3c5 Mon Sep 17 00:00:00 2001 From: X Code Date: Thu, 23 Dec 2021 17:13:21 -0800 Subject: [PATCH 146/179] Format code --- lib/src/widgets/delegate.dart | 2 +- lib/src/widgets/proxy.dart | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/delegate.dart b/lib/src/widgets/delegate.dart index bd681e76..8b98803e 100644 --- a/lib/src/widgets/delegate.dart +++ b/lib/src/widgets/delegate.dart @@ -1,8 +1,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import '../../flutter_quill.dart'; +import '../../flutter_quill.dart'; import 'text_selection.dart'; typedef EmbedBuilder = Widget Function( diff --git a/lib/src/widgets/proxy.dart b/lib/src/widgets/proxy.dart index 45a513b2..f002ec94 100644 --- a/lib/src/widgets/proxy.dart +++ b/lib/src/widgets/proxy.dart @@ -129,6 +129,7 @@ class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox { } class RichTextProxy extends SingleChildRenderObjectWidget { + /// Child argument should be an instance of RichText widget. const RichTextProxy( RichText child, this.textStyle, From 69979d86b5a9d6b725146458959afbccc606622b Mon Sep 17 00:00:00 2001 From: X Code Date: Thu, 23 Dec 2021 17:13:44 -0800 Subject: [PATCH 147/179] Remove QuillSimpleViewer --- lib/src/widgets/simple_viewer.dart | 367 ----------------------------- lib/widgets/simple_viewer.dart | 3 - 2 files changed, 370 deletions(-) delete mode 100644 lib/src/widgets/simple_viewer.dart delete mode 100644 lib/widgets/simple_viewer.dart diff --git a/lib/src/widgets/simple_viewer.dart b/lib/src/widgets/simple_viewer.dart deleted file mode 100644 index c48173af..00000000 --- a/lib/src/widgets/simple_viewer.dart +++ /dev/null @@ -1,367 +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: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'; -import 'video_app.dart'; -import 'youtube_video_app.dart'; - -class QuillSimpleViewer extends StatefulWidget { - const QuillSimpleViewer({ - required this.controller, - required this.readOnly, - 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; - final bool readOnly; - - @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, bool readOnly) { - 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)); - case 'video': - final videoUrl = node.value.data; - if (videoUrl.contains('youtube.com') || videoUrl.contains('youtu.be')) { - return YoutubeVideoApp( - videoUrl: videoUrl, context: context, readOnly: readOnly); - } - return VideoApp( - videoUrl: videoUrl, context: context, readOnly: readOnly); - 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, - cursorController: _cursorCont, - floatingCursorDisabled: true, - 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( - block: node, - textDirection: _textDirection, - scrollBottomInset: widget.scrollBottomInset, - verticalSpacing: _getVerticalSpacingForBlock(node, _styles), - textSelection: widget.controller.selection, - color: Colors.black, - styles: _styles, - enableInteractiveSelection: false, - hasFocus: false, - contentPadding: attrs.containsKey(Attribute.codeBlock.key) - ? const EdgeInsets.all(16) - : null, - embedBuilder: embedBuilder, - cursorCont: _cursorCont, - indentLevelCounts: indentLevelCounts, - onCheckboxTap: _handleCheckboxTap, - readOnly: widget.readOnly); - 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, - readOnly: widget.readOnly, - ); - 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; - } else if (attrs.containsKey(Attribute.list.key)) { - return defaultStyles!.lists!.verticalSpacing; - } else if (attrs.containsKey(Attribute.align.key)) { - return defaultStyles!.align!.verticalSpacing; - } - return const Tuple2(0, 0); - } - - 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, - required this.cursorController, - required this.floatingCursorDisabled, - this.offset, - this.padding = EdgeInsets.zero, - Key? key, - }) : super(key: key, children: children); - - final ViewportOffset? offset; - final Document document; - final TextDirection textDirection; - final LayerLink startHandleLayerLink; - final LayerLink endHandleLayerLink; - final TextSelectionChangedHandler onSelectionChanged; - final double scrollBottomInset; - final EdgeInsetsGeometry padding; - final CursorCont cursorController; - final bool floatingCursorDisabled; - - @override - RenderEditor createRenderObject(BuildContext context) { - return RenderEditor( - offset: offset, - document: document, - textDirection: textDirection, - hasFocus: false, - selection: const TextSelection(baseOffset: 0, extentOffset: 0), - startHandleLayerLink: startHandleLayerLink, - endHandleLayerLink: endHandleLayerLink, - onSelectionChanged: onSelectionChanged, - cursorController: cursorController, - padding: padding, - scrollBottomInset: scrollBottomInset, - floatingCursorDisabled: floatingCursorDisabled); - } - - @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/simple_viewer.dart b/lib/widgets/simple_viewer.dart deleted file mode 100644 index fbea8404..00000000 --- a/lib/widgets/simple_viewer.dart +++ /dev/null @@ -1,3 +0,0 @@ -/// TODO: Remove this file in the next breaking release, because implementation -/// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../src/widgets/simple_viewer.dart'; From 71793e5bd86ff8a566687b0f4214dacc9a7a37fb Mon Sep 17 00:00:00 2001 From: X Code Date: Thu, 23 Dec 2021 17:14:31 -0800 Subject: [PATCH 148/179] Add abstract class StyledNode --- lib/src/models/documents/nodes/node.dart | 37 +++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/lib/src/models/documents/nodes/node.dart b/lib/src/models/documents/nodes/node.dart index 08335727..8863138a 100644 --- a/lib/src/models/documents/nodes/node.dart +++ b/lib/src/models/documents/nodes/node.dart @@ -14,7 +14,8 @@ import 'line.dart'; /// The [offset] property is relative to [parent]. See also [documentOffset] /// which provides absolute offset of this node within the document. /// -/// The current parent node is exposed by the [parent] property. +/// The current parent node is exposed by the [parent] property. A node is +/// considered [mounted] when the [parent] property is not `null`. abstract class Node extends LinkedListEntry { /// Current parent of this node. May be null if this node is not mounted. Container? parent; @@ -119,6 +120,40 @@ abstract class Node extends LinkedListEntry { /// abstract methods end } +/// An interface for document nodes with style. +abstract class StyledNode implements Node { + /// Style of this node. + @override + Style get style; +} + +/// Mixin used by nodes that wish to implement [StyledNode] interface. +abstract class StyledNodeMixin implements StyledNode { + @override + Style get style => _style; + @override + Style _style = Style(); + + /// Applies style [attribute] to this node. + @override + void applyAttribute(Attribute attribute) { + _style = _style.merge(attribute); + } + + /// Applies new style [value] to this node. Provided [value] is merged + /// into current style. + @override + void applyStyle(Style value) { + _style = _style.mergeAll(value); + } + + /// Clears style of this node. + @override + void clearStyle() { + _style = Style(); + } +} + /// Root node of document tree. class Root extends Container> { @override From dfa623a4fdd477ad21a317bf14daaff93d530524 Mon Sep 17 00:00:00 2001 From: X Code Date: Thu, 23 Dec 2021 17:26:24 -0800 Subject: [PATCH 149/179] Implements StyledNode --- lib/src/models/documents/nodes/block.dart | 2 +- lib/src/models/documents/nodes/leaf.dart | 2 +- lib/src/models/documents/nodes/line.dart | 2 +- lib/src/models/documents/nodes/node.dart | 27 ----------------------- 4 files changed, 3 insertions(+), 30 deletions(-) diff --git a/lib/src/models/documents/nodes/block.dart b/lib/src/models/documents/nodes/block.dart index 095f1183..c69d6a8f 100644 --- a/lib/src/models/documents/nodes/block.dart +++ b/lib/src/models/documents/nodes/block.dart @@ -13,7 +13,7 @@ import 'node.dart'; /// - Text Alignment /// - Text Direction /// - Code Block -class Block extends Container { +class Block extends Container implements StyledNode { /// Creates new unmounted [Block]. @override Node newInstance() => Block(); diff --git a/lib/src/models/documents/nodes/leaf.dart b/lib/src/models/documents/nodes/leaf.dart index 3895fd62..48817a10 100644 --- a/lib/src/models/documents/nodes/leaf.dart +++ b/lib/src/models/documents/nodes/leaf.dart @@ -7,7 +7,7 @@ import 'line.dart'; import 'node.dart'; /// A leaf in Quill document tree. -abstract class Leaf extends Node { +abstract class Leaf extends Node implements StyledNode { /// Creates a new [Leaf] with specified [data]. factory Leaf(Object data) { if (data is Embeddable) { diff --git a/lib/src/models/documents/nodes/line.dart b/lib/src/models/documents/nodes/line.dart index bccd7fb8..b0977500 100644 --- a/lib/src/models/documents/nodes/line.dart +++ b/lib/src/models/documents/nodes/line.dart @@ -17,7 +17,7 @@ import 'node.dart'; /// /// When a line contains an embed, it fully occupies the line, no other embeds /// or text nodes are allowed. -class Line extends Container { +class Line extends Container implements StyledNode { @override Leaf get defaultChild => Text(); diff --git a/lib/src/models/documents/nodes/node.dart b/lib/src/models/documents/nodes/node.dart index 8863138a..090c1ba8 100644 --- a/lib/src/models/documents/nodes/node.dart +++ b/lib/src/models/documents/nodes/node.dart @@ -127,33 +127,6 @@ abstract class StyledNode implements Node { Style get style; } -/// Mixin used by nodes that wish to implement [StyledNode] interface. -abstract class StyledNodeMixin implements StyledNode { - @override - Style get style => _style; - @override - Style _style = Style(); - - /// Applies style [attribute] to this node. - @override - void applyAttribute(Attribute attribute) { - _style = _style.merge(attribute); - } - - /// Applies new style [value] to this node. Provided [value] is merged - /// into current style. - @override - void applyStyle(Style value) { - _style = _style.mergeAll(value); - } - - /// Clears style of this node. - @override - void clearStyle() { - _style = Style(); - } -} - /// Root node of document tree. class Root extends Container> { @override From 193dd68ccc9439c0fa880ebe33c27b7e25b9bfec Mon Sep 17 00:00:00 2001 From: X Code Date: Thu, 23 Dec 2021 17:28:26 -0800 Subject: [PATCH 150/179] Revert "Implements StyledNode" This reverts commit dfa623a4fdd477ad21a317bf14daaff93d530524. --- lib/src/models/documents/nodes/block.dart | 2 +- lib/src/models/documents/nodes/leaf.dart | 2 +- lib/src/models/documents/nodes/line.dart | 2 +- lib/src/models/documents/nodes/node.dart | 27 +++++++++++++++++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/src/models/documents/nodes/block.dart b/lib/src/models/documents/nodes/block.dart index c69d6a8f..095f1183 100644 --- a/lib/src/models/documents/nodes/block.dart +++ b/lib/src/models/documents/nodes/block.dart @@ -13,7 +13,7 @@ import 'node.dart'; /// - Text Alignment /// - Text Direction /// - Code Block -class Block extends Container implements StyledNode { +class Block extends Container { /// Creates new unmounted [Block]. @override Node newInstance() => Block(); diff --git a/lib/src/models/documents/nodes/leaf.dart b/lib/src/models/documents/nodes/leaf.dart index 48817a10..3895fd62 100644 --- a/lib/src/models/documents/nodes/leaf.dart +++ b/lib/src/models/documents/nodes/leaf.dart @@ -7,7 +7,7 @@ import 'line.dart'; import 'node.dart'; /// A leaf in Quill document tree. -abstract class Leaf extends Node implements StyledNode { +abstract class Leaf extends Node { /// Creates a new [Leaf] with specified [data]. factory Leaf(Object data) { if (data is Embeddable) { diff --git a/lib/src/models/documents/nodes/line.dart b/lib/src/models/documents/nodes/line.dart index b0977500..bccd7fb8 100644 --- a/lib/src/models/documents/nodes/line.dart +++ b/lib/src/models/documents/nodes/line.dart @@ -17,7 +17,7 @@ import 'node.dart'; /// /// When a line contains an embed, it fully occupies the line, no other embeds /// or text nodes are allowed. -class Line extends Container implements StyledNode { +class Line extends Container { @override Leaf get defaultChild => Text(); diff --git a/lib/src/models/documents/nodes/node.dart b/lib/src/models/documents/nodes/node.dart index 090c1ba8..8863138a 100644 --- a/lib/src/models/documents/nodes/node.dart +++ b/lib/src/models/documents/nodes/node.dart @@ -127,6 +127,33 @@ abstract class StyledNode implements Node { Style get style; } +/// Mixin used by nodes that wish to implement [StyledNode] interface. +abstract class StyledNodeMixin implements StyledNode { + @override + Style get style => _style; + @override + Style _style = Style(); + + /// Applies style [attribute] to this node. + @override + void applyAttribute(Attribute attribute) { + _style = _style.merge(attribute); + } + + /// Applies new style [value] to this node. Provided [value] is merged + /// into current style. + @override + void applyStyle(Style value) { + _style = _style.mergeAll(value); + } + + /// Clears style of this node. + @override + void clearStyle() { + _style = Style(); + } +} + /// Root node of document tree. class Root extends Container> { @override From c8b50d9e80fea5abbbf30cb878716bb25715e0ff Mon Sep 17 00:00:00 2001 From: X Code Date: Thu, 23 Dec 2021 17:30:18 -0800 Subject: [PATCH 151/179] Remove abstract class StyledNode --- lib/src/models/documents/nodes/node.dart | 34 ------------------------ 1 file changed, 34 deletions(-) diff --git a/lib/src/models/documents/nodes/node.dart b/lib/src/models/documents/nodes/node.dart index 8863138a..6805a3be 100644 --- a/lib/src/models/documents/nodes/node.dart +++ b/lib/src/models/documents/nodes/node.dart @@ -120,40 +120,6 @@ abstract class Node extends LinkedListEntry { /// abstract methods end } -/// An interface for document nodes with style. -abstract class StyledNode implements Node { - /// Style of this node. - @override - Style get style; -} - -/// Mixin used by nodes that wish to implement [StyledNode] interface. -abstract class StyledNodeMixin implements StyledNode { - @override - Style get style => _style; - @override - Style _style = Style(); - - /// Applies style [attribute] to this node. - @override - void applyAttribute(Attribute attribute) { - _style = _style.merge(attribute); - } - - /// Applies new style [value] to this node. Provided [value] is merged - /// into current style. - @override - void applyStyle(Style value) { - _style = _style.mergeAll(value); - } - - /// Clears style of this node. - @override - void clearStyle() { - _style = Style(); - } -} - /// Root node of document tree. class Root extends Container> { @override From 2080aab6a9905138e64de4c585d91170cc69dbe2 Mon Sep 17 00:00:00 2001 From: X Code Date: Thu, 23 Dec 2021 18:25:19 -0800 Subject: [PATCH 152/179] Launch link improvements Allows launching links in editing mode, where: * For desktop platforms: links launch on `Cmd` + `Click` (macOS) or `Ctrl` + `Click` (windows, linux) * For mobile platforms: long-pressing a link shows a context menu with multiple actions (Open, Copy, Remove) for the user to choose from. --- lib/src/widgets/editor.dart | 38 ++-- lib/src/widgets/keyboard_listener.dart | 89 +++++++++ lib/src/widgets/link.dart | 170 +++++++++++++++++ lib/src/widgets/raw_editor.dart | 40 +++- lib/src/widgets/text_block.dart | 15 +- lib/src/widgets/text_line.dart | 242 ++++++++++++++++++++++--- 6 files changed, 541 insertions(+), 53 deletions(-) create mode 100644 lib/src/widgets/keyboard_listener.dart create mode 100644 lib/src/widgets/link.dart diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index a6fb480d..b55047ae 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -9,9 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:string_validator/string_validator.dart'; -import 'package:url_launcher/url_launcher.dart'; -import '../models/documents/attribute.dart'; import '../models/documents/document.dart'; import '../models/documents/nodes/container.dart' as container_node; import '../models/documents/nodes/embed.dart'; @@ -25,6 +23,7 @@ import 'default_styles.dart'; import 'delegate.dart'; import 'float_cursor.dart'; import 'image.dart'; +import 'link.dart'; import 'raw_editor.dart'; import 'text_selection.dart'; import 'video_app.dart'; @@ -246,6 +245,7 @@ class QuillEditor extends StatefulWidget { this.onSingleLongTapMoveUpdate, this.onSingleLongTapEnd, this.embedBuilder = defaultEmbedBuilder, + this.linkActionPickerDelegate = defaultLinkActionPickerDelegate, this.customStyleBuilder, this.floatingCursorDisabled = false, Key? key}); @@ -312,6 +312,21 @@ class QuillEditor extends StatefulWidget { final EmbedBuilder embedBuilder; final CustomStyleBuilder? customStyleBuilder; + /// Delegate function responsible for showing menu with link actions on + /// mobile platforms (iOS, Android). + /// + /// The menu is triggered in editing mode ([readOnly] is set to `false`) + /// when the user long-presses a link-styled text segment. + /// + /// FlutterQuill provides default implementation which can be overridden by + /// this field to customize the user experience. + /// + /// By default on iOS the menu is displayed with [showCupertinoModalPopup] + /// which constructs an instance of [CupertinoActionSheet]. For Android, + /// the menu is displayed with [showModalBottomSheet] and a list of + /// Material [ListTile]s. + final LinkActionPickerDelegate linkActionPickerDelegate; + final bool floatingCursorDisabled; @override @@ -415,6 +430,7 @@ class _QuillEditorState extends State enableInteractiveSelection: widget.enableInteractiveSelection, scrollPhysics: widget.scrollPhysics, embedBuilder: widget.embedBuilder, + linkActionPickerDelegate: widget.linkActionPickerDelegate, customStyleBuilder: widget.customStyleBuilder, floatingCursorDisabled: widget.floatingCursorDisabled, ); @@ -520,20 +536,6 @@ class _QuillEditorSelectionGestureDetectorBuilder return false; } final segment = segmentResult.node as leaf.Leaf; - if (segment.style.containsKey(Attribute.link.key)) { - var launchUrl = getEditor()!.widget.onLaunchUrl; - launchUrl ??= _launchUrl; - String? link = segment.style.attributes[Attribute.link.key]!.value; - if (getEditor()!.widget.readOnly && link != null) { - link = link.trim(); - if (!linkPrefixes - .any((linkPrefix) => link!.toLowerCase().startsWith(linkPrefix))) { - link = 'https://$link'; - } - launchUrl(link); - } - return false; - } if (getEditor()!.widget.readOnly && segment.value is BlockEmbed) { final blockEmbed = segment.value as BlockEmbed; if (blockEmbed.type == 'image') { @@ -557,10 +559,6 @@ class _QuillEditorSelectionGestureDetectorBuilder return false; } - Future _launchUrl(String url) async { - await launch(url); - } - @override void onTapDown(TapDownDetails details) { if (_state.widget.onTapDown != null) { diff --git a/lib/src/widgets/keyboard_listener.dart b/lib/src/widgets/keyboard_listener.dart new file mode 100644 index 00000000..47cb9555 --- /dev/null +++ b/lib/src/widgets/keyboard_listener.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class QuillPressedKeys extends ChangeNotifier { + static QuillPressedKeys of(BuildContext context) { + final widget = + context.dependOnInheritedWidgetOfExactType<_QuillPressedKeysAccess>(); + return widget!.pressedKeys; + } + + bool _metaPressed = false; + bool _controlPressed = false; + + /// Whether meta key is currently pressed. + bool get metaPressed => _metaPressed; + + /// Whether control key is currently pressed. + bool get controlPressed => _controlPressed; + + void _updatePressedKeys(Set pressedKeys) { + final meta = pressedKeys.contains(LogicalKeyboardKey.metaLeft) || + pressedKeys.contains(LogicalKeyboardKey.metaRight); + final control = pressedKeys.contains(LogicalKeyboardKey.controlLeft) || + pressedKeys.contains(LogicalKeyboardKey.controlRight); + if (_metaPressed != meta || _controlPressed != control) { + _metaPressed = meta; + _controlPressed = control; + notifyListeners(); + } + } +} + +class QuillKeyboardListener extends StatefulWidget { + const QuillKeyboardListener({required this.child, Key? key}) + : super(key: key); + + final Widget child; + + @override + QuillKeyboardListenerState createState() => QuillKeyboardListenerState(); +} + +class QuillKeyboardListenerState extends State { + final QuillPressedKeys _pressedKeys = QuillPressedKeys(); + + bool _keyEvent(KeyEvent event) { + _pressedKeys + ._updatePressedKeys(HardwareKeyboard.instance.logicalKeysPressed); + return false; + } + + @override + void initState() { + super.initState(); + HardwareKeyboard.instance.addHandler(_keyEvent); + _pressedKeys + ._updatePressedKeys(HardwareKeyboard.instance.logicalKeysPressed); + } + + @override + void dispose() { + HardwareKeyboard.instance.removeHandler(_keyEvent); + _pressedKeys.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _QuillPressedKeysAccess( + pressedKeys: _pressedKeys, + child: widget.child, + ); + } +} + +class _QuillPressedKeysAccess extends InheritedWidget { + const _QuillPressedKeysAccess({ + required this.pressedKeys, + required Widget child, + Key? key, + }) : super(key: key, child: child); + + final QuillPressedKeys pressedKeys; + + @override + bool updateShouldNotify(covariant _QuillPressedKeysAccess oldWidget) { + return oldWidget.pressedKeys != pressedKeys; + } +} diff --git a/lib/src/widgets/link.dart b/lib/src/widgets/link.dart new file mode 100644 index 00000000..87378ca5 --- /dev/null +++ b/lib/src/widgets/link.dart @@ -0,0 +1,170 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../models/documents/nodes/node.dart'; + +/// List of possible actions returned from [LinkActionPickerDelegate]. +enum LinkMenuAction { + /// Launch the link + launch, + + /// Copy to clipboard + copy, + + /// Remove link style attribute + remove, + + /// No-op + none, +} + +/// Used internally by widget layer. +typedef LinkActionPicker = Future Function(Node linkNode); + +typedef LinkActionPickerDelegate = Future Function( + BuildContext context, String link); + +Future defaultLinkActionPickerDelegate( + BuildContext context, String link) async { + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + return _showCupertinoLinkMenu(context, link); + case TargetPlatform.android: + return _showMaterialMenu(context, link); + default: + assert( + false, + 'defaultShowLinkActionsMenu not supposed to ' + 'be invoked for $defaultTargetPlatform'); + return LinkMenuAction.none; + } +} + +Future _showCupertinoLinkMenu( + BuildContext context, String link) async { + final result = await showCupertinoModalPopup( + context: context, + builder: (ctx) { + return CupertinoActionSheet( + title: Text(link), + actions: [ + _CupertinoAction( + title: 'Open', + icon: Icons.language_sharp, + onPressed: () => Navigator.of(context).pop(LinkMenuAction.launch), + ), + _CupertinoAction( + title: 'Copy', + icon: Icons.copy_sharp, + onPressed: () => Navigator.of(context).pop(LinkMenuAction.copy), + ), + _CupertinoAction( + title: 'Remove', + icon: Icons.link_off_sharp, + onPressed: () => Navigator.of(context).pop(LinkMenuAction.remove), + ), + ], + ); + }, + ); + return result ?? LinkMenuAction.none; +} + +class _CupertinoAction extends StatelessWidget { + const _CupertinoAction({ + required this.title, + required this.icon, + required this.onPressed, + Key? key, + }) : super(key: key); + + final String title; + final IconData icon; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return CupertinoActionSheetAction( + onPressed: onPressed, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + children: [ + Expanded( + child: Text( + title, + textAlign: TextAlign.start, + style: TextStyle(color: theme.colorScheme.onSurface), + ), + ), + Icon( + icon, + size: theme.iconTheme.size, + color: theme.colorScheme.onSurface.withOpacity(0.75), + ) + ], + ), + ), + ); + } +} + +Future _showMaterialMenu( + BuildContext context, String link) async { + final result = await showModalBottomSheet( + context: context, + builder: (ctx) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _MaterialAction( + title: 'Open', + icon: Icons.language_sharp, + onPressed: () => Navigator.of(context).pop(LinkMenuAction.launch), + ), + _MaterialAction( + title: 'Copy', + icon: Icons.copy_sharp, + onPressed: () => Navigator.of(context).pop(LinkMenuAction.copy), + ), + _MaterialAction( + title: 'Remove', + icon: Icons.link_off_sharp, + onPressed: () => Navigator.of(context).pop(LinkMenuAction.remove), + ), + ], + ); + }, + ); + + return result ?? LinkMenuAction.none; +} + +class _MaterialAction extends StatelessWidget { + const _MaterialAction({ + required this.title, + required this.icon, + required this.onPressed, + Key? key, + }) : super(key: key); + + final String title; + final IconData icon; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return ListTile( + leading: Icon( + icon, + size: theme.iconTheme.size, + color: theme.colorScheme.onSurface.withOpacity(0.75), + ), + title: Text(title), + onTap: onPressed, + ); + } +} diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index e8cf766d..506020a1 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -12,6 +12,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:tuple/tuple.dart'; +import '../../models/documents/nodes/node.dart'; import '../models/documents/attribute.dart'; import '../models/documents/document.dart'; import '../models/documents/nodes/block.dart'; @@ -21,6 +22,8 @@ import 'cursor.dart'; import 'default_styles.dart'; import 'delegate.dart'; import 'editor.dart'; +import 'keyboard_listener.dart'; +import 'link.dart'; import 'proxy.dart'; import 'quill_single_child_scroll_view.dart'; import 'raw_editor/raw_editor_state_selection_delegate_mixin.dart'; @@ -62,6 +65,7 @@ class RawEditor extends StatefulWidget { this.enableInteractiveSelection = true, this.scrollPhysics, this.embedBuilder = defaultEmbedBuilder, + this.linkActionPickerDelegate = defaultLinkActionPickerDelegate, this.customStyleBuilder, this.floatingCursorDisabled = false}) : assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'), @@ -76,9 +80,25 @@ class RawEditor extends StatefulWidget { final bool scrollable; final double scrollBottomInset; final EdgeInsetsGeometry padding; + + /// Whether the text can be changed. + /// + /// When this is set to true, the text cannot be modified + /// by any shortcut or keyboard operation. The text is still selectable. + /// + /// Defaults to false. Must not be null. final bool readOnly; + final String? placeholder; + + /// Callback which is triggered when the user wants to open a URL from + /// a link in the document. final ValueChanged? onLaunchUrl; + + /// Configuration of toolbar options. + /// + /// By default, all options are enabled. If [readOnly] is true, + /// paste and cut will be disabled regardless. final ToolbarOptions toolbarOptions; final bool showSelectionHandles; final bool showCursor; @@ -95,6 +115,7 @@ class RawEditor extends StatefulWidget { final bool enableInteractiveSelection; final ScrollPhysics? scrollPhysics; final EmbedBuilder embedBuilder; + final LinkActionPickerDelegate linkActionPickerDelegate; final CustomStyleBuilder? customStyleBuilder; final bool floatingCursorDisabled; @@ -218,9 +239,11 @@ class RawEditorState extends EditorState data: _styles!, child: MouseRegion( cursor: SystemMouseCursors.text, - child: Container( - constraints: constraints, - child: child, + child: QuillKeyboardListener( + child: Container( + constraints: constraints, + child: child, + ), ), ), ); @@ -270,6 +293,7 @@ class RawEditorState extends EditorState final attrs = node.style.attributes; final editableTextBlock = EditableTextBlock( block: node, + controller: widget.controller, textDirection: _textDirection, scrollBottomInset: widget.scrollBottomInset, verticalSpacing: _getVerticalSpacingForBlock(node, _styles), @@ -282,6 +306,8 @@ class RawEditorState extends EditorState ? const EdgeInsets.all(16) : null, embedBuilder: widget.embedBuilder, + linkActionPicker: _linkActionPicker, + onLaunchUrl: widget.onLaunchUrl, cursorCont: _cursorCont, indentLevelCounts: indentLevelCounts, onCheckboxTap: _handleCheckboxTap, @@ -304,6 +330,9 @@ class RawEditorState extends EditorState customStyleBuilder: widget.customStyleBuilder, styles: _styles!, readOnly: widget.readOnly, + controller: widget.controller, + linkActionPicker: _linkActionPicker, + onLaunchUrl: widget.onLaunchUrl, ); final editableTextLine = EditableTextLine( node, @@ -592,6 +621,11 @@ class RawEditorState extends EditorState }); } + Future _linkActionPicker(Node linkNode) async { + final link = linkNode.style.attributes[Attribute.link.key]!.value!; + return widget.linkActionPickerDelegate(context, link); + } + bool _showCaretOnScreenScheduled = false; void _showCaretOnScreen() { diff --git a/lib/src/widgets/text_block.dart b/lib/src/widgets/text_block.dart index fcfaa195..819ebe07 100644 --- a/lib/src/widgets/text_block.dart +++ b/lib/src/widgets/text_block.dart @@ -2,15 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:tuple/tuple.dart'; -import '../models/documents/attribute.dart'; +import '../../flutter_quill.dart'; import '../models/documents/nodes/block.dart'; import '../models/documents/nodes/line.dart'; -import '../widgets/style_widgets/style_widgets.dart'; import 'box.dart'; import 'cursor.dart'; -import 'default_styles.dart'; import 'delegate.dart'; -import 'editor.dart'; +import 'link.dart'; import 'text_line.dart'; import 'text_selection.dart'; @@ -49,6 +47,7 @@ const List romanNumbers = [ class EditableTextBlock extends StatelessWidget { const EditableTextBlock( {required this.block, + required this.controller, required this.textDirection, required this.scrollBottomInset, required this.verticalSpacing, @@ -59,14 +58,17 @@ class EditableTextBlock extends StatelessWidget { required this.hasFocus, required this.contentPadding, required this.embedBuilder, + required this.linkActionPicker, required this.cursorCont, required this.indentLevelCounts, required this.onCheckboxTap, required this.readOnly, + this.onLaunchUrl, this.customStyleBuilder, Key? key}); final Block block; + final QuillController controller; final TextDirection textDirection; final double scrollBottomInset; final Tuple2 verticalSpacing; @@ -77,6 +79,8 @@ class EditableTextBlock extends StatelessWidget { final bool hasFocus; final EdgeInsets? contentPadding; final EmbedBuilder embedBuilder; + final LinkActionPicker linkActionPicker; + final ValueChanged? onLaunchUrl; final CustomStyleBuilder? customStyleBuilder; final CursorCont cursorCont; final Map indentLevelCounts; @@ -128,6 +132,9 @@ class EditableTextBlock extends StatelessWidget { customStyleBuilder: customStyleBuilder, styles: styles!, readOnly: readOnly, + controller: controller, + linkActionPicker: linkActionPicker, + onLaunchUrl: onLaunchUrl, ), _getIndentWidth(), _getSpacingForLine(line, index, count, defaultStyles), diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index a9e2dbc5..0b2e8b3e 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -2,31 +2,37 @@ import 'dart:collection'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; +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'; +import '../../flutter_quill.dart'; import '../models/documents/nodes/container.dart' as container; import '../models/documents/nodes/leaf.dart' as leaf; -import '../models/documents/nodes/leaf.dart'; import '../models/documents/nodes/line.dart'; import '../models/documents/nodes/node.dart'; import '../models/documents/style.dart'; import '../utils/color.dart'; import 'box.dart'; import 'cursor.dart'; -import 'default_styles.dart'; import 'delegate.dart'; +import 'keyboard_listener.dart'; +import 'link.dart'; import 'proxy.dart'; import 'text_selection.dart'; -class TextLine extends StatelessWidget { +class TextLine extends StatefulWidget { const TextLine({ required this.line, required this.embedBuilder, required this.styles, required this.readOnly, + required this.controller, + required this.onLaunchUrl, + required this.linkActionPicker, this.textDirection, this.customStyleBuilder, Key? key, @@ -37,23 +43,109 @@ class TextLine extends StatelessWidget { final EmbedBuilder embedBuilder; final DefaultStyles styles; final bool readOnly; + final QuillController controller; final CustomStyleBuilder? customStyleBuilder; + final ValueChanged? onLaunchUrl; + final LinkActionPicker linkActionPicker; + + @override + State createState() => _TextLineState(); +} + +class _TextLineState extends State { + bool _metaOrControlPressed = false; + + UniqueKey _richTextKey = UniqueKey(); + + final _linkRecognizers = {}; + + QuillPressedKeys? _pressedKeys; + + void _pressedKeysChanged() { + final newValue = _pressedKeys!.metaPressed || _pressedKeys!.controlPressed; + if (_metaOrControlPressed != newValue) { + setState(() { + _metaOrControlPressed = newValue; + _richTextKey = UniqueKey(); + }); + } + } + + bool get isDesktop => { + TargetPlatform.macOS, + TargetPlatform.linux, + TargetPlatform.windows + }.contains(defaultTargetPlatform); + + bool get canLaunchLinks { + // In readOnly mode users can launch links + // by simply tapping (clicking) on them + if (widget.readOnly) return true; + + // In editing mode it depends on the platform: + + // Desktop platforms (macos, linux, windows): + // only allow Meta(Control)+Click combinations + if (isDesktop) { + return _metaOrControlPressed; + } + // Mobile platforms (ios, android): always allow but we install a + // long-press handler instead of a tap one. LongPress is followed by a + // context menu with actions. + return true; + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_pressedKeys == null) { + _pressedKeys = QuillPressedKeys.of(context); + _pressedKeys!.addListener(_pressedKeysChanged); + } else { + _pressedKeys!.removeListener(_pressedKeysChanged); + _pressedKeys = QuillPressedKeys.of(context); + _pressedKeys!.addListener(_pressedKeysChanged); + } + } + + @override + void didUpdateWidget(covariant TextLine oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.readOnly != widget.readOnly) { + _richTextKey = UniqueKey(); + _linkRecognizers + ..forEach((key, value) { + value.dispose(); + }) + ..clear(); + } + } + + @override + void dispose() { + _pressedKeys?.removeListener(_pressedKeysChanged); + _linkRecognizers + ..forEach((key, value) => value.dispose()) + ..clear(); + super.dispose(); + } @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); - if (line.hasEmbed && line.childCount == 1) { + if (widget.line.hasEmbed && widget.line.childCount == 1) { // For video, it is always single child - final embed = line.children.single as Embed; - return EmbedProxy(embedBuilder(context, embed, readOnly)); + final embed = widget.line.children.single as Embed; + return EmbedProxy(widget.embedBuilder(context, embed, widget.readOnly)); } final textSpan = _getTextSpanForWholeLine(context); final strutStyle = StrutStyle.fromTextStyle(textSpan.style!); final textAlign = _getTextAlign(); final child = RichText( + key: _richTextKey, text: textSpan, textAlign: textAlign, - textDirection: textDirection, + textDirection: widget.textDirection, strutStyle: strutStyle, textScaleFactor: MediaQuery.textScaleFactorOf(context), ); @@ -61,7 +153,7 @@ class TextLine extends StatelessWidget { child, textSpan.style!, textAlign, - textDirection!, + widget.textDirection!, 1, Localizations.localeOf(context), strutStyle, @@ -70,23 +162,25 @@ class TextLine extends StatelessWidget { } InlineSpan _getTextSpanForWholeLine(BuildContext context) { - final lineStyle = _getLineStyle(styles); - if (!line.hasEmbed) { - return _buildTextSpan(styles, line.children, lineStyle); + final lineStyle = _getLineStyle(widget.styles); + if (!widget.line.hasEmbed) { + return _buildTextSpan(widget.styles, widget.line.children, lineStyle); } // The line could contain more than one Embed & more than one Text final textSpanChildren = []; var textNodes = LinkedList(); - for (final child in line.children) { + for (final child in widget.line.children) { if (child is Embed) { if (textNodes.isNotEmpty) { - textSpanChildren.add(_buildTextSpan(styles, textNodes, lineStyle)); + textSpanChildren + .add(_buildTextSpan(widget.styles, textNodes, lineStyle)); textNodes = LinkedList(); } // Here it should be image final embed = WidgetSpan( - child: EmbedProxy(embedBuilder(context, child, readOnly))); + child: EmbedProxy( + widget.embedBuilder(context, child, widget.readOnly))); textSpanChildren.add(embed); continue; } @@ -96,14 +190,14 @@ class TextLine extends StatelessWidget { } if (textNodes.isNotEmpty) { - textSpanChildren.add(_buildTextSpan(styles, textNodes, lineStyle)); + textSpanChildren.add(_buildTextSpan(widget.styles, textNodes, lineStyle)); } return TextSpan(style: lineStyle, children: textSpanChildren); } TextAlign _getTextAlign() { - final alignment = line.style.attributes[Attribute.align.key]; + final alignment = widget.line.style.attributes[Attribute.align.key]; if (alignment == Attribute.leftAlignment) { return TextAlign.start; } else if (alignment == Attribute.centerAlignment) { @@ -119,7 +213,8 @@ class TextLine extends StatelessWidget { TextSpan _buildTextSpan(DefaultStyles defaultStyles, LinkedList nodes, TextStyle lineStyle) { final children = nodes - .map((node) => _getTextSpanFromNode(defaultStyles, node, line.style)) + .map((node) => + _getTextSpanFromNode(defaultStyles, node, widget.line.style)) .toList(growable: false); return TextSpan(children: children, style: lineStyle); @@ -128,11 +223,11 @@ class TextLine extends StatelessWidget { TextStyle _getLineStyle(DefaultStyles defaultStyles) { var textStyle = const TextStyle(); - if (line.style.containsKey(Attribute.placeholder.key)) { + if (widget.line.style.containsKey(Attribute.placeholder.key)) { return defaultStyles.placeHolder!.style; } - final header = line.style.attributes[Attribute.header.key]; + final header = widget.line.style.attributes[Attribute.header.key]; final m = { Attribute.h1: defaultStyles.h1!.style, Attribute.h2: defaultStyles.h2!.style, @@ -143,7 +238,7 @@ class TextLine extends StatelessWidget { // Only retrieve exclusive block format for the line style purpose Attribute? block; - line.style.getBlocksExceptHeader().forEach((key, value) { + widget.line.style.getBlocksExceptHeader().forEach((key, value) { if (Attribute.exclusiveBlockKeys.contains(key)) { block = value; } @@ -159,21 +254,21 @@ class TextLine extends StatelessWidget { } textStyle = textStyle.merge(toMerge); - textStyle = _applyCustomAttributes(textStyle, line.style.attributes); + textStyle = _applyCustomAttributes(textStyle, widget.line.style.attributes); return textStyle; } TextStyle _applyCustomAttributes( TextStyle textStyle, Map attributes) { - if (customStyleBuilder == null) { + if (widget.customStyleBuilder == null) { return textStyle; } attributes.keys.forEach((key) { final attr = attributes[key]; if (attr != null) { /// Custom Attribute - final customAttr = customStyleBuilder!.call(attr); + final customAttr = widget.customStyleBuilder!.call(attr); textStyle = textStyle.merge(customAttr); } }); @@ -184,6 +279,7 @@ class TextLine extends StatelessWidget { DefaultStyles defaultStyles, Node node, Style lineStyle) { final textNode = node as leaf.Text; final nodeStyle = textNode.style; + final isLink = nodeStyle.containsKey(Attribute.link.key); var res = const TextStyle(); // This is inline text style final color = textNode.style.attributes[Attribute.color.key]; var hasLink = false; @@ -268,14 +364,108 @@ class TextLine extends StatelessWidget { } res = _applyCustomAttributes(res, textNode.style.attributes); - if (hasLink && readOnly) { + if (hasLink && widget.readOnly) { return TextSpan( text: textNode.value, style: res, mouseCursor: SystemMouseCursors.click, ); } - return TextSpan(text: textNode.value, style: res); + return TextSpan( + text: textNode.value, + style: res, + recognizer: isLink && canLaunchLinks ? _getRecognizer(node) : null, + mouseCursor: isLink && canLaunchLinks ? SystemMouseCursors.click : null, + ); + } + + GestureRecognizer _getRecognizer(Node segment) { + if (_linkRecognizers.containsKey(segment)) { + return _linkRecognizers[segment]!; + } + + if (isDesktop || widget.readOnly) { + _linkRecognizers[segment] = TapGestureRecognizer() + ..onTap = () => _tapNodeLink(segment); + } else { + _linkRecognizers[segment] = LongPressGestureRecognizer() + ..onLongPress = () => _longPressLink(segment); + } + return _linkRecognizers[segment]!; + } + + Future _launchUrl(String url) async { + await launch(url); + } + + void _tapNodeLink(Node node) { + final link = node.style.attributes[Attribute.link.key]!.value; + + _tapLink(link); + } + + void _tapLink(String? link) { + if (widget.readOnly || link == null) { + return; + } + + var launchUrl = widget.onLaunchUrl; + launchUrl ??= _launchUrl; + + link = link.trim(); + if (!linkPrefixes + .any((linkPrefix) => link!.toLowerCase().startsWith(linkPrefix))) { + link = 'https://$link'; + } + launchUrl(link); + } + + Future _longPressLink(Node node) async { + final link = node.style.attributes[Attribute.link.key]!.value!; + final action = await widget.linkActionPicker(node); + switch (action) { + case LinkMenuAction.launch: + _tapLink(link); + break; + case LinkMenuAction.copy: + // ignore: unawaited_futures + Clipboard.setData(ClipboardData(text: link)); + break; + case LinkMenuAction.remove: + final range = _getLinkRange(node); + widget.controller + .formatText(range.start, range.end - range.start, Attribute.link); + break; + case LinkMenuAction.none: + break; + } + } + + TextRange _getLinkRange(Node node) { + var start = node.documentOffset; + var length = node.length; + var prev = node.previous; + final linkAttr = node.style.attributes[Attribute.link.key]!; + while (prev != null) { + if (prev.style.attributes[Attribute.link.key] == linkAttr) { + start = prev.documentOffset; + length += prev.length; + prev = prev.previous; + } else { + break; + } + } + + var next = node.next; + while (next != null) { + if (next.style.attributes[Attribute.link.key] == linkAttr) { + length += next.length; + next = next.next; + } else { + break; + } + } + return TextRange(start: start, end: start + length); } TextStyle _merge(TextStyle a, TextStyle b) { From 7c13740ca093f822b82ea0e3262d5bd36f6eea31 Mon Sep 17 00:00:00 2001 From: X Code Date: Thu, 23 Dec 2021 18:27:27 -0800 Subject: [PATCH 153/179] Upgrade to 3.0.0 --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfc6b171..b7544e56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [3.0.0] +* Launch link improvements. +* Removed QuillSimpleViewer. + ## [2.5.2] * Skip image when pasting. diff --git a/pubspec.yaml b/pubspec.yaml index 08930761..9f31e4a7 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: 2.5.2 +version: 3.0.0 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 6d4de8e33234f62bf6825d487b94e2684ca7c878 Mon Sep 17 00:00:00 2001 From: X Code Date: Thu, 23 Dec 2021 18:36:13 -0800 Subject: [PATCH 154/179] Export link.dart --- lib/flutter_quill.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/flutter_quill.dart b/lib/flutter_quill.dart index bc1343ee..8b0a76ee 100644 --- a/lib/flutter_quill.dart +++ b/lib/flutter_quill.dart @@ -10,5 +10,6 @@ export 'src/models/themes/quill_icon_theme.dart'; export 'src/widgets/controller.dart'; export 'src/widgets/default_styles.dart'; export 'src/widgets/editor.dart'; +export 'src/widgets/link.dart' show LinkActionPickerDelegate, LinkMenuAction; export 'src/widgets/style_widgets/style_widgets.dart'; export 'src/widgets/toolbar.dart'; From a19b468a1d60865034010956a4ac356c9f9f00db Mon Sep 17 00:00:00 2001 From: li3317 Date: Thu, 23 Dec 2021 22:40:16 -0500 Subject: [PATCH 155/179] rename header to heading for inline code --- lib/src/widgets/default_styles.dart | 34 ++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/src/widgets/default_styles.dart b/lib/src/widgets/default_styles.dart index dab9cc03..2edc2088 100644 --- a/lib/src/widgets/default_styles.dart +++ b/lib/src/widgets/default_styles.dart @@ -59,9 +59,9 @@ class DefaultTextBlockStyle { class InlineCodeStyle { InlineCodeStyle({ required this.style, - this.heading1, - this.heading2, - this.heading3, + this.header1, + this.header2, + this.header3, this.backgroundColor, this.radius, }); @@ -69,14 +69,14 @@ class InlineCodeStyle { /// Base text style for an inline code. final TextStyle style; - /// Style override for inline code in headings level 1. - final TextStyle? heading1; + /// Style override for inline code in header level 1. + final TextStyle? header1; /// Style override for inline code in headings level 2. - final TextStyle? heading2; + final TextStyle? header2; /// Style override for inline code in headings level 3. - final TextStyle? heading3; + final TextStyle? header3; /// Background color for inline code. final Color? backgroundColor; @@ -88,13 +88,13 @@ class InlineCodeStyle { /// [lineStyle]. TextStyle styleFor(Style lineStyle) { if (lineStyle.containsKey(Attribute.h1.key)) { - return heading1 ?? style; + return header1 ?? style; } if (lineStyle.containsKey(Attribute.h2.key)) { - return heading2 ?? style; + return header2 ?? style; } if (lineStyle.containsKey(Attribute.h3.key)) { - return heading3 ?? style; + return header3 ?? style; } return style; } @@ -108,16 +108,16 @@ class InlineCodeStyle { return false; } return other.style == style && - other.heading1 == heading1 && - other.heading2 == heading2 && - other.heading3 == heading3 && + other.header1 == header1 && + other.header2 == header2 && + other.header3 == header3 && other.backgroundColor == backgroundColor && other.radius == radius; } @override int get hashCode => - Object.hash(style, heading1, heading2, heading3, backgroundColor, radius); + Object.hash(style, header1, header2, header3, backgroundColor, radius); } class DefaultListBlockStyle extends DefaultTextBlockStyle { @@ -255,12 +255,12 @@ class DefaultStyles { backgroundColor: Colors.grey.shade100, radius: const Radius.circular(3), style: inlineCodeStyle, - heading1: inlineCodeStyle.copyWith( + header1: inlineCodeStyle.copyWith( fontSize: 32, fontWeight: FontWeight.w300, ), - heading2: inlineCodeStyle.copyWith(fontSize: 22), - heading3: inlineCodeStyle.copyWith( + header2: inlineCodeStyle.copyWith(fontSize: 22), + header3: inlineCodeStyle.copyWith( fontSize: 18, fontWeight: FontWeight.w500, ), From 6a4a8e20e4f0bbe63df3931f9d4a5f45b6fbeb55 Mon Sep 17 00:00:00 2001 From: X Code Date: Fri, 24 Dec 2021 16:35:04 -0800 Subject: [PATCH 156/179] Handle null value of Attribute.link --- lib/src/widgets/text_line.dart | 36 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index 0b2e8b3e..9a451a94 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -279,10 +279,22 @@ class _TextLineState extends State { DefaultStyles defaultStyles, Node node, Style lineStyle) { final textNode = node as leaf.Text; final nodeStyle = textNode.style; - final isLink = nodeStyle.containsKey(Attribute.link.key); + final isLink = nodeStyle.containsKey(Attribute.link.key) && + nodeStyle.attributes[Attribute.link.key]!.value != null; + + return TextSpan( + text: textNode.value, + style: _getInlineTextStyle( + textNode, defaultStyles, nodeStyle, lineStyle, isLink), + recognizer: isLink && canLaunchLinks ? _getRecognizer(node) : null, + mouseCursor: isLink && canLaunchLinks ? SystemMouseCursors.click : null, + ); + } + + TextStyle _getInlineTextStyle(leaf.Text textNode, DefaultStyles defaultStyles, + Style nodeStyle, Style lineStyle, bool isLink) { var res = const TextStyle(); // This is inline text style final color = textNode.style.attributes[Attribute.color.key]; - var hasLink = false; { Attribute.bold.key: defaultStyles.bold, @@ -300,10 +312,10 @@ class _TextLineState extends State { } res = _merge(res.copyWith(decorationColor: textColor), s!.copyWith(decorationColor: textColor)); + } else if (k == Attribute.link.key && !isLink) { + // null value for link should be ignored + // i.e. nodeStyle.attributes[Attribute.link.key]!.value == null } else { - if (k == Attribute.link.key) { - hasLink = true; - } res = _merge(res, s!); } } @@ -364,19 +376,7 @@ class _TextLineState extends State { } res = _applyCustomAttributes(res, textNode.style.attributes); - if (hasLink && widget.readOnly) { - return TextSpan( - text: textNode.value, - style: res, - mouseCursor: SystemMouseCursors.click, - ); - } - return TextSpan( - text: textNode.value, - style: res, - recognizer: isLink && canLaunchLinks ? _getRecognizer(node) : null, - mouseCursor: isLink && canLaunchLinks ? SystemMouseCursors.click : null, - ); + return res; } GestureRecognizer _getRecognizer(Node segment) { From a1aac335c6d5be04ed69c690a39ab4f8f7f9dad2 Mon Sep 17 00:00:00 2001 From: X Code Date: Fri, 24 Dec 2021 18:10:31 -0800 Subject: [PATCH 157/179] Upgrade to 3.0.1 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7544e56..452e0fc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [3.0.1] +* Handle null value of Attribute.link. + ## [3.0.0] * Launch link improvements. * Removed QuillSimpleViewer. From 69a307948e059f941b194c3963c8413bfdc548af Mon Sep 17 00:00:00 2001 From: X Code Date: Fri, 24 Dec 2021 18:11:15 -0800 Subject: [PATCH 158/179] Upgrade to 3.0.1 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 9f31e4a7..1a9f4e02 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: 3.0.0 +version: 3.0.1 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 6ff299118dd1cc8aa6ffc9fe86149d85209e08fe Mon Sep 17 00:00:00 2001 From: li3317 Date: Sun, 26 Dec 2021 00:28:04 -0500 Subject: [PATCH 159/179] fix launch link for read only --- CHANGELOG.md | 3 +++ lib/src/widgets/text_line.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 452e0fc1..9b39d5f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [3.0.2] +* Fix lanch link for read-only mode. + ## [3.0.1] * Handle null value of Attribute.link. diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index 9a451a94..32648d8f 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -405,7 +405,7 @@ class _TextLineState extends State { } void _tapLink(String? link) { - if (widget.readOnly || link == null) { + if (!widget.readOnly || link == null) { return; } diff --git a/pubspec.yaml b/pubspec.yaml index 1a9f4e02..0605a27c 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: 3.0.1 +version: 3.0.2 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 736de5ef181e4e74d11b67aafc9509ba790fb17b Mon Sep 17 00:00:00 2001 From: Arshak Aghakaryan Date: Mon, 27 Dec 2021 20:25:14 +0400 Subject: [PATCH 160/179] Do not show caret on screen when the editor is not focused (#555) --- lib/src/widgets/raw_editor.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 506020a1..556716cc 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -422,7 +422,7 @@ class RawEditorState extends EditorState _keyboardVisibilityController?.onChange.listen((visible) { _keyboardVisible = visible; if (visible) { - _onChangeTextEditingValue(); + _onChangeTextEditingValue(!_hasFocus); } }); } From 56ef9f46d0444700731a45c9027dd15cf505e470 Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 27 Dec 2021 08:37:00 -0800 Subject: [PATCH 161/179] Upgrade to 3.0.3 --- CHANGELOG.md | 5 ++++- pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b39d5f6..d08f4ebd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ +# [3.0.3] +* Do not show caret on screen when the editor is not focused. + # [3.0.2] -* Fix lanch link for read-only mode. +* Fix launch link for read-only mode. ## [3.0.1] * Handle null value of Attribute.link. diff --git a/pubspec.yaml b/pubspec.yaml index 0605a27c..e3624607 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: 3.0.2 +version: 3.0.3 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From d934c1a8828b7b495cdf0e5e785942b98a9a2936 Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 27 Dec 2021 08:52:13 -0800 Subject: [PATCH 162/179] Update constructor of RichTextProxy --- lib/src/widgets/proxy.dart | 21 +++++++++++---------- lib/src/widgets/text_line.dart | 15 ++++++--------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/lib/src/widgets/proxy.dart b/lib/src/widgets/proxy.dart index f002ec94..b9f683e1 100644 --- a/lib/src/widgets/proxy.dart +++ b/lib/src/widgets/proxy.dart @@ -131,16 +131,17 @@ class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox { class RichTextProxy extends SingleChildRenderObjectWidget { /// Child argument should be an instance of RichText widget. const RichTextProxy( - RichText child, - this.textStyle, - this.textAlign, - this.textDirection, - this.textScaleFactor, - this.locale, - this.strutStyle, - this.textWidthBasis, - this.textHeightBehavior, - ) : super(child: child); + {required RichText child, + required this.textStyle, + required this.textAlign, + required this.textDirection, + required this.locale, + required this.strutStyle, + this.textScaleFactor = 1.0, + this.textWidthBasis = TextWidthBasis.parent, + this.textHeightBehavior, + Key? key}) + : super(key: key, child: child); final TextStyle textStyle; final TextAlign textAlign; diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index 32648d8f..db0fc973 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -150,15 +150,12 @@ class _TextLineState extends State { textScaleFactor: MediaQuery.textScaleFactorOf(context), ); return RichTextProxy( - child, - textSpan.style!, - textAlign, - widget.textDirection!, - 1, - Localizations.localeOf(context), - strutStyle, - TextWidthBasis.parent, - null); + textStyle: textSpan.style!, + textAlign: textAlign, + textDirection: widget.textDirection!, + strutStyle: strutStyle, + locale: Localizations.localeOf(context), + child: child); } InlineSpan _getTextSpanForWholeLine(BuildContext context) { From 05f8b6342b8b0f3c507714bda90bc649706faf9c Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 27 Dec 2021 11:02:42 -0800 Subject: [PATCH 163/179] Add comments --- lib/src/widgets/raw_editor.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 556716cc..6374beef 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -548,11 +548,19 @@ class RawEditorState extends EditorState _cursorCont.startOrStopCursorTimerIfNeeded( _hasFocus, widget.controller.selection); if (hasConnection) { + // To keep the cursor from blinking while typing, we want to restart the + // cursor timer every time a new character is typed. _cursorCont ..stopCursorTimer(resetCharTicks: false) ..startCursorTimer(); } + // Refresh selection overlay after the build step had a chance to + // update and register all children of RenderEditor. Otherwise this will + // fail in situations where a new line of text is entered, which adds + // a new RenderEditableBox child. If we try to update selection overlay + // immediately it'll not be able to find the new child since it hasn't been + // built yet. SchedulerBinding.instance!.addPostFrameCallback((_) { if (!mounted) { return; From c49af0d4d29f8b5a9221bbb33bdec1f21ebacb09 Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 27 Dec 2021 11:07:08 -0800 Subject: [PATCH 164/179] Add comments --- lib/src/widgets/text_selection.dart | 63 +++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/lib/src/widgets/text_selection.dart b/lib/src/widgets/text_selection.dart index 593a8135..35d5e059 100644 --- a/lib/src/widgets/text_selection.dart +++ b/lib/src/widgets/text_selection.dart @@ -19,6 +19,8 @@ TextSelection localSelection(Node node, TextSelection selection, fromParent) { extentOffset: math.min(selection.end - offset, node.length - 1)); } +/// The text position that a give selection handle manipulates. Dragging the +/// [start] handle always moves the [start]/[baseOffset] of the selection. enum _TextSelectionHandlePosition { START, END } /// internal use, used to get drag direction information @@ -56,7 +58,14 @@ class DragTextSelection extends TextSelection { } } +/// An object that manages a pair of text selection handles. +/// +/// The selection handles are displayed in the [Overlay] that most closely +/// encloses the given [BuildContext]. class EditorTextSelectionOverlay { + /// Creates an object that manages overlay entries for selection handles. + /// + /// The [context] must not be null and must have an [Overlay] as an ancestor. EditorTextSelectionOverlay( this.value, this.handlesVisible, @@ -80,16 +89,70 @@ class EditorTextSelectionOverlay { TextEditingValue value; bool handlesVisible = false; + + /// The context in which the selection handles should appear. + /// + /// This context must have an [Overlay] as an ancestor because this object + /// will display the text selection handles in that [Overlay]. final BuildContext context; + + /// Debugging information for explaining why the [Overlay] is required. final Widget debugRequiredFor; + + /// The object supplied to the [CompositedTransformTarget] that wraps the text + /// field. final LayerLink toolbarLayerLink; + + /// The objects supplied to the [CompositedTransformTarget] that wraps the + /// location of start selection handle. final LayerLink startHandleLayerLink; + + /// The objects supplied to the [CompositedTransformTarget] that wraps the + /// location of end selection handle. final LayerLink endHandleLayerLink; + + /// The editable line in which the selected text is being displayed. final RenderEditor? renderObject; + + /// Builds text selection handles and toolbar. final TextSelectionControls selectionCtrls; + + /// The delegate for manipulating the current selection in the owning + /// text field. final TextSelectionDelegate selectionDelegate; + + /// Determines the way that drag start behavior is handled. + /// + /// If set to [DragStartBehavior.start], handle drag behavior will + /// begin upon the detection of a drag gesture. If set to + /// [DragStartBehavior.down] it will begin when a down event is first + /// detected. + /// + /// In general, setting this to [DragStartBehavior.start] will make drag + /// animation smoother and setting it to [DragStartBehavior.down] will make + /// drag behavior feel slightly more reactive. + /// + /// By default, the drag start behavior is [DragStartBehavior.start]. + /// + /// See also: + /// + /// * [DragGestureRecognizer.dragStartBehavior], + /// which gives an example for the different behaviors. final DragStartBehavior dragStartBehavior; + + /// {@template flutter.widgets.textSelection.onSelectionHandleTapped} + /// A callback that's invoked when a selection handle is tapped. + /// + /// Both regular taps and long presses invoke this callback, but a drag + /// gesture won't. + /// {@endtemplate} final VoidCallback? onSelectionHandleTapped; + + /// Maintains the status of the clipboard for determining if its contents can + /// be pasted or not. + /// + /// Useful because the actual value of the clipboard can only be checked + /// asynchronously (see [Clipboard.getData]). final ClipboardStatusNotifier clipboardStatus; late AnimationController _toolbarController; List? _handles; From 9d6681ed2b3734bec56c8e763953de004c3e4ce8 Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 27 Dec 2021 11:16:30 -0800 Subject: [PATCH 165/179] Update constructor of EditorTextSelectionOverlay --- lib/src/widgets/raw_editor.dart | 24 ++++++++++-------------- lib/src/widgets/text_selection.dart | 28 ++++++++++++++-------------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 6374beef..006b6989 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -4,7 +4,6 @@ import 'dart:math' as math; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; @@ -588,19 +587,16 @@ class RawEditorState extends EditorState _selectionOverlay = null; _selectionOverlay = EditorTextSelectionOverlay( - textEditingValue, - false, - context, - widget, - _toolbarLayerLink, - _startHandleLayerLink, - _endHandleLayerLink, - getRenderEditor(), - widget.selectionCtrls, - this, - DragStartBehavior.start, - null, - _clipboardStatus, + value: textEditingValue, + context: context, + debugRequiredFor: widget, + toolbarLayerLink: _toolbarLayerLink, + startHandleLayerLink: _startHandleLayerLink, + endHandleLayerLink: _endHandleLayerLink, + renderObject: getRenderEditor(), + selectionCtrls: widget.selectionCtrls, + selectionDelegate: this, + clipboardStatus: _clipboardStatus, ); _selectionOverlay!.handlesVisible = _shouldShowSelectionHandles(); _selectionOverlay!.showHandles(); diff --git a/lib/src/widgets/text_selection.dart b/lib/src/widgets/text_selection.dart index 35d5e059..d77ad93e 100644 --- a/lib/src/widgets/text_selection.dart +++ b/lib/src/widgets/text_selection.dart @@ -66,21 +66,21 @@ class EditorTextSelectionOverlay { /// Creates an object that manages overlay entries for selection handles. /// /// The [context] must not be null and must have an [Overlay] as an ancestor. - EditorTextSelectionOverlay( - this.value, - this.handlesVisible, - this.context, - this.debugRequiredFor, - this.toolbarLayerLink, - this.startHandleLayerLink, - this.endHandleLayerLink, - this.renderObject, - this.selectionCtrls, - this.selectionDelegate, - this.dragStartBehavior, + EditorTextSelectionOverlay({ + required this.value, + required this.context, + required this.toolbarLayerLink, + required this.startHandleLayerLink, + required this.endHandleLayerLink, + required this.renderObject, + required this.debugRequiredFor, + required this.selectionCtrls, + required this.selectionDelegate, + required this.clipboardStatus, this.onSelectionHandleTapped, - this.clipboardStatus, - ) { + this.dragStartBehavior = DragStartBehavior.start, + this.handlesVisible = false, + }) { final overlay = Overlay.of(context, rootOverlay: true)!; _toolbarController = AnimationController( From b2fb04e59ea762ed758c1c0b246fbe217da86f55 Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 27 Dec 2021 11:24:11 -0800 Subject: [PATCH 166/179] Add _TransparentTapGestureRecognizer --- lib/src/widgets/text_selection.dart | 58 +++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/lib/src/widgets/text_selection.dart b/lib/src/widgets/text_selection.dart index d77ad93e..a057a7b9 100644 --- a/lib/src/widgets/text_selection.dart +++ b/lib/src/widgets/text_selection.dart @@ -88,6 +88,21 @@ class EditorTextSelectionOverlay { } TextEditingValue value; + + /// Whether selection handles are visible. + /// + /// Set to false if you want to hide the handles. Use this property to show or + /// hide the handle without rebuilding them. + /// + /// If this method is called while the [SchedulerBinding.schedulerPhase] is + /// [SchedulerPhase.persistentCallbacks], i.e. during the build, layout, or + /// paint phases (see [WidgetsBinding.drawFrame]), then the update is delayed + /// until the post-frame callbacks phase. Otherwise the update is done + /// synchronously. This means that it is safe to call during builds, but also + /// that if you do call this during a build, the UI will not update until the + /// next frame (i.e. many milliseconds later). + /// + /// Defaults to false. bool handlesVisible = false; /// The context in which the selection handles should appear. @@ -155,7 +170,12 @@ class EditorTextSelectionOverlay { /// asynchronously (see [Clipboard.getData]). final ClipboardStatusNotifier clipboardStatus; late AnimationController _toolbarController; + + /// A pair of handles. If this is non-null, there are always 2, though the + /// second is hidden when the selection is collapsed. List? _handles; + + /// A copy/paste toolbar. OverlayEntry? toolbar; TextSelection get _selection => value.selection; @@ -776,9 +796,12 @@ class _EditorTextSelectionGestureDetectorState Widget build(BuildContext context) { final gestures = {}; - gestures[TapGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(debugOwner: this), + // Use _TransparentTapGestureRecognizer so that TextSelectionGestureDetector + // can receive the same tap events that a selection handle placed visually + // on top of it also receives. + gestures[_TransparentTapGestureRecognizer] = + GestureRecognizerFactoryWithHandlers<_TransparentTapGestureRecognizer>( + () => _TransparentTapGestureRecognizer(debugOwner: this), (instance) { instance ..onTapDown = _handleTapDown @@ -843,3 +866,32 @@ class _EditorTextSelectionGestureDetectorState ); } } + +// A TapGestureRecognizer which allows other GestureRecognizers to win in the +// GestureArena. This means both _TransparentTapGestureRecognizer and other +// GestureRecognizers can handle the same event. +// +// This enables proper handling of events on both the selection handle and the +// underlying input, since there is significant overlap between the two given +// the handle's padded hit area. For example, the selection handle needs to +// handle single taps on itself, but double taps need to be handled by the +// underlying input. +class _TransparentTapGestureRecognizer extends TapGestureRecognizer { + _TransparentTapGestureRecognizer({ + Object? debugOwner, + }) : super(debugOwner: debugOwner); + + @override + void rejectGesture(int pointer) { + // Accept new gestures that another recognizer has already won. + // Specifically, this needs to accept taps on the text selection handle on + // behalf of the text field in order to handle double tap to select. It must + // not accept other gestures like longpresses and drags that end outside of + // the text field. + if (state == GestureRecognizerState.ready) { + acceptGesture(pointer); + } else { + super.rejectGesture(pointer); + } + } +} From 6b2b2aa57837e464c7f9e67525bf3657ae2c4d4f Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 27 Dec 2021 18:38:08 -0800 Subject: [PATCH 167/179] Add comment --- lib/src/widgets/delegate.dart | 174 +++++++++++++++++++++++++++++++++- lib/src/widgets/editor.dart | 3 +- 2 files changed, 173 insertions(+), 4 deletions(-) diff --git a/lib/src/widgets/delegate.dart b/lib/src/widgets/delegate.dart index 8b98803e..c64d2dab 100644 --- a/lib/src/widgets/delegate.dart +++ b/lib/src/widgets/delegate.dart @@ -18,29 +18,94 @@ abstract class EditorTextSelectionGestureDetectorBuilderDelegate { bool getSelectionEnabled(); } +/// Builds a [EditorTextSelectionGestureDetector] to wrap an [EditableText]. +/// +/// The class implements sensible defaults for many user interactions +/// with an [EditableText] (see the documentation of the various gesture handler +/// methods, e.g. [onTapDown], [onForcePressStart], etc.). Subclasses of +/// [EditorTextSelectionGestureDetectorBuilder] can change the behavior +/// performed in responds to these gesture events by overriding +/// the corresponding handler methods of this class. +/// +/// The resulting [EditorTextSelectionGestureDetector] to wrap an [EditableText] +/// is obtained by calling [buildGestureDetector]. +/// +/// See also: +/// +/// * [TextField], which uses a subclass to implement the Material-specific +/// gesture logic of an [EditableText]. +/// * [CupertinoTextField], which uses a subclass to implement the +/// Cupertino-specific gesture logic of an [EditableText]. class EditorTextSelectionGestureDetectorBuilder { - EditorTextSelectionGestureDetectorBuilder(this.delegate); + /// Creates a [EditorTextSelectionGestureDetectorBuilder]. + /// + /// The [delegate] must not be null. + EditorTextSelectionGestureDetectorBuilder({required this.delegate}); + /// The delegate for this [EditorTextSelectionGestureDetectorBuilder]. + /// + /// The delegate provides the builder with information about what actions can + /// currently be performed on the textfield. Based on this, the builder adds + /// the correct gesture handlers to the gesture detector. + @protected final EditorTextSelectionGestureDetectorBuilderDelegate delegate; + + /// Whether to show the selection toolbar. + /// + /// It is based on the signal source when a [onTapDown] is called. This getter + /// will return true if current [onTapDown] event is triggered by a touch or + /// a stylus. bool shouldShowSelectionToolbar = true; + /// The [State] of the [EditableText] for which the builder will provide a + /// [EditorTextSelectionGestureDetector]. + @protected EditorState? getEditor() { return delegate.getEditableTextKey().currentState; } + /// The [RenderObject] of the [EditableText] for which the builder will + /// provide a [EditorTextSelectionGestureDetector]. + @protected RenderEditor? getRenderEditor() { return getEditor()!.getRenderEditor(); } + /// Handler for [EditorTextSelectionGestureDetector.onTapDown]. + /// + /// By default, it forwards the tap to [RenderEditable.handleTapDown] and sets + /// [shouldShowSelectionToolbar] to true if the tap was initiated by a finger + /// or stylus. + /// + /// See also: + /// + /// * [EditorTextSelectionGestureDetector.onTapDown], + /// which triggers this callback. + @protected void onTapDown(TapDownDetails details) { getRenderEditor()!.handleTapDown(details); - + // The selection overlay should only be shown when the user is interacting + // through a touch screen (via either a finger or a stylus). + // A mouse shouldn't trigger the selection overlay. + // For backwards-compatibility, we treat a null kind the same as touch. final kind = details.kind; shouldShowSelectionToolbar = kind == null || kind == PointerDeviceKind.touch || kind == PointerDeviceKind.stylus; } + /// Handler for [EditorTextSelectionGestureDetector.onForcePressStart]. + /// + /// By default, it selects the word at the position of the force press, + /// if selection is enabled. + /// + /// This callback is only applicable when force press is enabled. + /// + /// See also: + /// + /// * [EditorTextSelectionGestureDetector.onForcePressStart], + /// which triggers this callback. + @protected void onForcePressStart(ForcePressDetails details) { assert(delegate.getForcePressEnabled()); shouldShowSelectionToolbar = true; @@ -53,6 +118,18 @@ class EditorTextSelectionGestureDetectorBuilder { } } + /// Handler for [EditorTextSelectionGestureDetector.onForcePressEnd]. + /// + /// By default, it selects words in the range specified in [details] and shows + /// toolbar if it is necessary. + /// + /// This callback is only applicable when force press is enabled. + /// + /// See also: + /// + /// * [EditorTextSelectionGestureDetector.onForcePressEnd], + /// which triggers this callback. + @protected void onForcePressEnd(ForcePressDetails details) { assert(delegate.getForcePressEnabled()); getRenderEditor()!.selectWordsInRange( @@ -65,14 +142,44 @@ class EditorTextSelectionGestureDetectorBuilder { } } + /// Handler for [EditorTextSelectionGestureDetector.onSingleTapUp]. + /// + /// By default, it selects word edge if selection is enabled. + /// + /// See also: + /// + /// * [EditorTextSelectionGestureDetector.onSingleTapUp], which triggers + /// this callback. + @protected void onSingleTapUp(TapUpDetails details) { if (delegate.getSelectionEnabled()) { getRenderEditor()!.selectWordEdge(SelectionChangedCause.tap); } } - void onSingleTapCancel() {} + /// Handler for [EditorTextSelectionGestureDetector.onSingleTapCancel]. + /// + /// By default, it services as place holder to enable subclass override. + /// + /// See also: + /// + /// * [EditorTextSelectionGestureDetector.onSingleTapCancel], which triggers + /// this callback. + @protected + void onSingleTapCancel() { + /* Subclass should override this method if needed. */ + } + /// Handler for [EditorTextSelectionGestureDetector.onSingleLongTapStart]. + /// + /// By default, it selects text position specified in [details] if selection + /// is enabled. + /// + /// See also: + /// + /// * [EditorTextSelectionGestureDetector.onSingleLongTapStart], + /// which triggers this callback. + @protected void onSingleLongTapStart(LongPressStartDetails details) { if (delegate.getSelectionEnabled()) { getRenderEditor()!.selectPositionAt( @@ -82,6 +189,16 @@ class EditorTextSelectionGestureDetectorBuilder { } } + /// Handler for [EditorTextSelectionGestureDetector.onSingleLongTapMoveUpdate] + /// + /// By default, it updates the selection location specified in [details] if + /// selection is enabled. + /// + /// See also: + /// + /// * [EditorTextSelectionGestureDetector.onSingleLongTapMoveUpdate], which + /// triggers this callback. + @protected void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { if (delegate.getSelectionEnabled()) { getRenderEditor()!.selectPositionAt( @@ -91,12 +208,31 @@ class EditorTextSelectionGestureDetectorBuilder { } } + /// Handler for [EditorTextSelectionGestureDetector.onSingleLongTapEnd]. + /// + /// By default, it shows toolbar if necessary. + /// + /// See also: + /// + /// * [EditorTextSelectionGestureDetector.onSingleLongTapEnd], + /// which triggers this callback. + @protected void onSingleLongTapEnd(LongPressEndDetails details) { if (shouldShowSelectionToolbar) { getEditor()!.showToolbar(); } } + /// Handler for [EditorTextSelectionGestureDetector.onDoubleTapDown]. + /// + /// By default, it selects a word through [RenderEditable.selectWord] if + /// selectionEnabled and shows toolbar if necessary. + /// + /// See also: + /// + /// * [EditorTextSelectionGestureDetector.onDoubleTapDown], + /// which triggers this callback. + @protected void onDoubleTapDown(TapDownDetails details) { if (delegate.getSelectionEnabled()) { getRenderEditor()!.selectWord(SelectionChangedCause.tap); @@ -106,20 +242,52 @@ class EditorTextSelectionGestureDetectorBuilder { } } + /// Handler for [EditorTextSelectionGestureDetector.onDragSelectionStart]. + /// + /// By default, it selects a text position specified in [details]. + /// + /// See also: + /// + /// * [EditorTextSelectionGestureDetector.onDragSelectionStart], + /// which triggers this callback. + @protected void onDragSelectionStart(DragStartDetails details) { getRenderEditor()!.handleDragStart(details); } + /// Handler for [EditorTextSelectionGestureDetector.onDragSelectionUpdate]. + /// + /// By default, it updates the selection location specified in the provided + /// details objects. + /// + /// See also: + /// + /// * [EditorTextSelectionGestureDetector.onDragSelectionUpdate], + /// which triggers this callback./lib/src/material/text_field.dart + @protected void onDragSelectionUpdate( DragStartDetails startDetails, DragUpdateDetails updateDetails) { getRenderEditor()!.extendSelection(updateDetails.globalPosition, cause: SelectionChangedCause.drag); } + /// Handler for [EditorTextSelectionGestureDetector.onDragSelectionEnd]. + /// + /// By default, it services as place holder to enable subclass override. + /// + /// See also: + /// + /// * [EditorTextSelectionGestureDetector.onDragSelectionEnd], + /// which triggers this callback. + @protected void onDragSelectionEnd(DragEndDetails details) { getRenderEditor()!.handleDragEnd(details); } + /// Returns a [EditorTextSelectionGestureDetector] configured with + /// the handlers provided by this builder. + /// + /// The [child] or its subtree should contain [EditableText]. Widget build(HitTestBehavior behavior, Widget child) { return EditorTextSelectionGestureDetector( onTapDown: onTapDown, diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index b55047ae..41317604 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -463,7 +463,8 @@ class _QuillEditorState extends State class _QuillEditorSelectionGestureDetectorBuilder extends EditorTextSelectionGestureDetectorBuilder { - _QuillEditorSelectionGestureDetectorBuilder(this._state) : super(_state); + _QuillEditorSelectionGestureDetectorBuilder(this._state) + : super(delegate: _state); final _QuillEditorState _state; From 0c43c8afcbf2664486ee31a2fa30e6737abf85a6 Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 27 Dec 2021 18:41:09 -0800 Subject: [PATCH 168/179] Update SelectionGestureDetectorBuilder.build method --- lib/src/widgets/delegate.dart | 4 +++- lib/src/widgets/editor.dart | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/src/widgets/delegate.dart b/lib/src/widgets/delegate.dart index c64d2dab..6b80cd68 100644 --- a/lib/src/widgets/delegate.dart +++ b/lib/src/widgets/delegate.dart @@ -288,8 +288,10 @@ class EditorTextSelectionGestureDetectorBuilder { /// the handlers provided by this builder. /// /// The [child] or its subtree should contain [EditableText]. - Widget build(HitTestBehavior behavior, Widget child) { + Widget build( + {required HitTestBehavior behavior, required Widget child, Key? key}) { return EditorTextSelectionGestureDetector( + key: key, onTapDown: onTapDown, onForcePressStart: delegate.getForcePressEnabled() ? onForcePressStart : null, diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 41317604..283ac14a 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -436,8 +436,8 @@ class _QuillEditorState extends State ); return _selectionGestureDetectorBuilder.build( - HitTestBehavior.translucent, - child, + behavior: HitTestBehavior.translucent, + child: child, ); } From 091bea1cd71bf21b17dfb929af95131c223c4d74 Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 27 Dec 2021 20:11:46 -0800 Subject: [PATCH 169/179] Add comments --- lib/src/widgets/raw_editor.dart | 2 +- lib/src/widgets/text_selection.dart | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 006b6989..c531ff88 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -513,7 +513,7 @@ class RawEditorState extends EditorState } void _updateSelectionOverlayForScroll() { - _selectionOverlay?.markNeedsBuild(); + _selectionOverlay?.updateForScroll(); } void _didChangeTextEditingValue([bool ignoreFocus = false]) { diff --git a/lib/src/widgets/text_selection.dart b/lib/src/widgets/text_selection.dart index a057a7b9..fd7e704a 100644 --- a/lib/src/widgets/text_selection.dart +++ b/lib/src/widgets/text_selection.dart @@ -187,6 +187,8 @@ class EditorTextSelectionOverlay { return; } handlesVisible = visible; + // If we are in build state, it will be too late to update visibility. + // We will need to schedule the build in next frame. if (SchedulerBinding.instance!.schedulerPhase == SchedulerPhase.persistentCallbacks) { SchedulerBinding.instance!.addPostFrameCallback(markNeedsBuild); @@ -195,6 +197,7 @@ class EditorTextSelectionOverlay { } } + /// Destroys the handles by removing them from overlay. void hideHandles() { if (_handles == null) { return; @@ -204,6 +207,9 @@ class EditorTextSelectionOverlay { _handles = null; } + /// Hides the toolbar part of the overlay. + /// + /// To hide the whole overlay, see [hide]. void hideToolbar() { assert(toolbar != null); _toolbarController.stop(); @@ -211,6 +217,7 @@ class EditorTextSelectionOverlay { toolbar = null; } + /// Shows the toolbar by inserting it into the [context]'s overlay. void showToolbar() { assert(toolbar == null); toolbar = OverlayEntry(builder: _buildToolbar); @@ -242,6 +249,15 @@ class EditorTextSelectionOverlay { )); } + /// Updates the overlay after the selection has changed. + /// + /// If this method is called while the [SchedulerBinding.schedulerPhase] is + /// [SchedulerPhase.persistentCallbacks], i.e. during the build, layout, or + /// paint phases (see [WidgetsBinding.drawFrame]), then the update is delayed + /// until the post-frame callbacks phase. Otherwise the update is done + /// synchronously. This means that it is safe to call during builds, but also + /// that if you do call this during a build, the UI will not update until the + /// next frame (i.e. many milliseconds later). void update(TextEditingValue newValue) { if (value == newValue) { return; @@ -291,6 +307,7 @@ class EditorTextSelectionOverlay { } Widget _buildToolbar(BuildContext context) { + // Find the horizontal midpoint, just above the selected text. final endpoints = renderObject!.getEndpointsForSelection(_selection); final editingRegion = Rect.fromPoints( @@ -341,6 +358,7 @@ class EditorTextSelectionOverlay { toolbar?.markNeedsBuild(); } + /// Hides the entire overlay including the toolbar and the handles. void hide() { if (_handles != null) { _handles![0].remove(); @@ -352,11 +370,13 @@ class EditorTextSelectionOverlay { } } + /// Final cleanup. void dispose() { hide(); _toolbarController.dispose(); } + /// Builds the handles by inserting them into the [context]'s overlay. void showHandles() { assert(_handles == null); _handles = [ @@ -371,8 +391,17 @@ class EditorTextSelectionOverlay { Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)! .insertAll(_handles!); } + + /// Causes the overlay to update its rendering. + /// + /// This is intended to be called when the [renderObject] may have changed its + /// text metrics (e.g. because the text was scrolled). + void updateForScroll() { + markNeedsBuild(); + } } +/// This widget represents a single draggable text selection handle. class _TextSelectionHandleOverlay extends StatefulWidget { const _TextSelectionHandleOverlay({ required this.selection, From ed1ceb58f07bf13e865753b4f18486a62514149c Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 27 Dec 2021 20:14:52 -0800 Subject: [PATCH 170/179] Add comments --- lib/src/widgets/raw_editor.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index c531ff88..a29fc90a 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -699,6 +699,10 @@ class RawEditorState extends EditorState getRenderEditor()!.debugAssertLayoutUpToDate(); } + /// Shows the selection toolbar at the location of the current cursor. + /// + /// Returns `false` if a toolbar couldn't be shown, such as when the toolbar + /// is already shown, or when no text selection currently exists. @override bool showToolbar() { // Web is using native dom elements to enable clipboard functionality of the From 907cb623b02ce4a9ab05b909745e2876cc16a94d Mon Sep 17 00:00:00 2001 From: Cheryl Date: Tue, 28 Dec 2021 10:59:14 -0800 Subject: [PATCH 171/179] Clean up code --- lib/src/widgets/raw_editor.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index a29fc90a..76afb863 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -584,7 +584,6 @@ class RawEditorState extends EditorState } } else if (_hasFocus) { _selectionOverlay?.hide(); - _selectionOverlay = null; _selectionOverlay = EditorTextSelectionOverlay( value: textEditingValue, From 47dd10b1dbb671bca253233799b091f116373691 Mon Sep 17 00:00:00 2001 From: X Code Date: Tue, 28 Dec 2021 15:26:21 -0800 Subject: [PATCH 172/179] Add comments --- lib/src/widgets/editor.dart | 2 + lib/src/widgets/raw_editor.dart | 91 +++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 283ac14a..fb4a86cd 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -619,6 +619,8 @@ class _QuillEditorSelectionGestureDetectorBuilder break; case PointerDeviceKind.touch: case PointerDeviceKind.unknown: + // On macOS/iOS/iPadOS a touch tap places the cursor at the edge + // of the word. try { getRenderEditor()!.selectWordEdge(SelectionChangedCause.tap); } finally { diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 76afb863..9a0b86dc 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -73,11 +73,17 @@ class RawEditor extends StatefulWidget { 'maxHeight cannot be null'), showCursor = showCursor ?? true, super(key: key); + + /// Controls the document being edited. final QuillController controller; + + /// Controls whether this editor has keyboard focus. final FocusNode focusNode; final ScrollController scrollController; final bool scrollable; final double scrollBottomInset; + + /// Additional space around the editor contents. final EdgeInsetsGeometry padding; /// Whether the text can be changed. @@ -99,20 +105,105 @@ class RawEditor extends StatefulWidget { /// By default, all options are enabled. If [readOnly] is true, /// paste and cut will be disabled regardless. final ToolbarOptions toolbarOptions; + + /// Whether to show selection handles. + /// + /// When a selection is active, there will be two handles at each side of + /// boundary, or one handle if the selection is collapsed. The handles can be + /// dragged to adjust the selection. + /// + /// See also: + /// + /// * [showCursor], which controls the visibility of the cursor. final bool showSelectionHandles; + + /// Whether to show cursor. + /// + /// The cursor refers to the blinking caret when the editor is focused. + /// + /// See also: + /// + /// * [cursorStyle], which controls the cursor visual representation. + /// * [showSelectionHandles], which controls the visibility of the selection + /// handles. final bool showCursor; + + /// The style to be used for the editing cursor. final CursorStyle cursorStyle; + + /// Configures how the platform keyboard will select an uppercase or + /// lowercase keyboard. + /// + /// Only supports text keyboards, other keyboard types will ignore this + /// configuration. Capitalization is locale-aware. + /// + /// Defaults to [TextCapitalization.none]. Must not be null. + /// + /// See also: + /// + /// * [TextCapitalization], for a description of each capitalization behavior final TextCapitalization textCapitalization; + + /// The maximum height this editor can have. + /// + /// If this is null then there is no limit to the editor's height and it will + /// expand to fill its parent. final double? maxHeight; + + /// The minimum height this editor can have. final double? minHeight; final DefaultStyles? customStyles; + + /// Whether this widget's height will be sized to fill its parent. + /// + /// If set to true and wrapped in a parent widget like [Expanded] or + /// + /// Defaults to false. final bool expands; + + /// Whether this editor should focus itself if nothing else is already + /// focused. + /// + /// If true, the keyboard will open as soon as this text field obtains focus. + /// Otherwise, the keyboard is only shown after the user taps the text field. + /// + /// Defaults to false. Cannot be null. final bool autoFocus; + + /// The color to use when painting the selection. final Color selectionColor; + + /// Delegate for building the text selection handles and toolbar. + /// + /// The [RawEditor] widget used on its own will not trigger the display + /// of the selection toolbar by itself. The toolbar is shown by calling + /// [RawEditorState.showToolbar] in response to an appropriate user event. final TextSelectionControls selectionCtrls; + + /// The appearance of the keyboard. + /// + /// This setting is only honored on iOS devices. + /// + /// Defaults to [Brightness.light]. final Brightness keyboardAppearance; + + /// If true, then long-pressing this TextField will select text and show the + /// cut/copy/paste menu, and tapping will move the text caret. + /// + /// True by default. + /// + /// If false, most of the accessibility support for selecting text, copy + /// and paste, and moving the caret will be disabled. final bool enableInteractiveSelection; + + /// The [ScrollPhysics] to use when vertically scrolling the input. + /// + /// If not specified, it will behave according to the current platform. + /// + /// See [Scrollable.physics]. final ScrollPhysics? scrollPhysics; + + /// Builder function for embeddable objects. final EmbedBuilder embedBuilder; final LinkActionPickerDelegate linkActionPickerDelegate; final CustomStyleBuilder? customStyleBuilder; From 6de4b1c7ef2d6d47be33a8cb2dae9c9e531bc86e Mon Sep 17 00:00:00 2001 From: X Code Date: Tue, 28 Dec 2021 18:25:23 -0800 Subject: [PATCH 173/179] Add comment --- lib/src/widgets/editor.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index fb4a86cd..30af9b0d 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -46,6 +46,8 @@ const linkPrefixes = [ 'http' ]; +/// Base interface for the editor state which defines contract used by +/// various mixins. abstract class EditorState extends State implements TextSelectionDelegate { ScrollController get scrollController; From f94b1382e109962180655c021e753b4146501873 Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 29 Dec 2021 16:02:21 -0800 Subject: [PATCH 174/179] Rename _resolvePadding() to resolvePadding() --- lib/src/widgets/text_line.dart | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index db0fc973..ba119529 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -742,11 +742,13 @@ class RenderEditableTextLine extends RenderEditableBox { }).toList(growable: false); } - void _resolvePadding() { + void resolvePadding() { if (_resolvedPadding != null) { return; } _resolvedPadding = padding.resolve(textDirection); + _resolvedPadding = _resolvedPadding!.copyWith(left: _resolvedPadding!.left); + assert(_resolvedPadding!.isNonNegative); } @@ -948,7 +950,7 @@ class RenderEditableTextLine extends RenderEditableBox { @override double computeMinIntrinsicWidth(double height) { - _resolvePadding(); + resolvePadding(); final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; final leadingWidth = _leading == null @@ -964,7 +966,7 @@ class RenderEditableTextLine extends RenderEditableBox { @override double computeMaxIntrinsicWidth(double height) { - _resolvePadding(); + resolvePadding(); final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; final leadingWidth = _leading == null @@ -980,7 +982,7 @@ class RenderEditableTextLine extends RenderEditableBox { @override double computeMinIntrinsicHeight(double width) { - _resolvePadding(); + resolvePadding(); final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; if (_body != null) { @@ -993,7 +995,7 @@ class RenderEditableTextLine extends RenderEditableBox { @override double computeMaxIntrinsicHeight(double width) { - _resolvePadding(); + resolvePadding(); final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; if (_body != null) { @@ -1006,7 +1008,7 @@ class RenderEditableTextLine extends RenderEditableBox { @override double computeDistanceToActualBaseline(TextBaseline baseline) { - _resolvePadding(); + resolvePadding(); return _body!.getDistanceToActualBaseline(baseline)! + _resolvedPadding!.top; } @@ -1016,7 +1018,7 @@ class RenderEditableTextLine extends RenderEditableBox { final constraints = this.constraints; _selectedRects = null; - _resolvePadding(); + resolvePadding(); assert(_resolvedPadding != null); if (_body == null && _leading == null) { From 2be716f68e50f6fe1997ca6fad3cf228664c4bcc Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 29 Dec 2021 18:35:09 -0800 Subject: [PATCH 175/179] Add comments --- lib/src/widgets/editor.dart | 96 +++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 30af9b0d..4bcb1621 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -270,26 +270,122 @@ class QuillEditor extends StatefulWidget { ); } + /// Controller object which establishes a link between a rich text document + /// and this editor. + /// + /// Must not be null. final QuillController controller; + + /// Controls whether this editor has keyboard focus. final FocusNode focusNode; + + /// The [ScrollController] to use when vertically scrolling the contents. final ScrollController scrollController; + + /// Whether this editor should create a scrollable container for its content. + /// + /// When set to `true` the editor's height can be controlled by [minHeight], + /// [maxHeight] and [expands] properties. + /// + /// When set to `false` the editor always expands to fit the entire content + /// of the document and should normally be placed as a child of another + /// scrollable widget, otherwise the content may be clipped. final bool scrollable; final double scrollBottomInset; + + /// Additional space around the content of this editor. final EdgeInsetsGeometry padding; + + /// Whether this editor should focus itself if nothing else is already + /// focused. + /// + /// If true, the keyboard will open as soon as this editor obtains focus. + /// Otherwise, the keyboard is only shown after the user taps the editor. + /// + /// Defaults to `false`. Cannot be `null`. final bool autoFocus; + + /// Whether to show cursor. + /// + /// The cursor refers to the blinking caret when the editor is focused. final bool? showCursor; final bool? paintCursorAboveText; + + /// Whether the text can be changed. + /// + /// When this is set to `true`, the text cannot be modified + /// by any shortcut or keyboard operation. The text is still selectable. + /// + /// Defaults to `false`. Must not be `null`. final bool readOnly; final String? placeholder; + + /// Whether to enable user interface affordances for changing the + /// text selection. + /// + /// For example, setting this to true will enable features such as + /// long-pressing the editor to select text and show the + /// cut/copy/paste menu, and tapping to move the text cursor. + /// + /// When this is false, the text selection cannot be adjusted by + /// the user, text cannot be copied, and the user cannot paste into + /// the text field from the clipboard. final bool enableInteractiveSelection; + + /// The minimum height to be occupied by this editor. + /// + /// This only has effect if [scrollable] is set to `true` and [expands] is + /// set to `false`. final double? minHeight; + + /// The maximum height to be occupied by this editor. + /// + /// This only has effect if [scrollable] is set to `true` and [expands] is + /// set to `false`. final double? maxHeight; final DefaultStyles? customStyles; + + /// Whether this editor's height will be sized to fill its parent. + /// + /// This only has effect if [scrollable] is set to `true`. + /// + /// If expands is set to true and wrapped in a parent widget like [Expanded] + /// or [SizedBox], the editor will expand to fill the parent. + /// + /// [maxHeight] and [minHeight] must both be `null` when this is set to + /// `true`. + /// + /// Defaults to `false`. final bool expands; + + /// Configures how the platform keyboard will select an uppercase or + /// lowercase keyboard. + /// + /// Only supports text keyboards, other keyboard types will ignore this + /// configuration. Capitalization is locale-aware. + /// + /// Defaults to [TextCapitalization.sentences]. Must not be `null`. final TextCapitalization textCapitalization; + + /// The appearance of the keyboard. + /// + /// This setting is only honored on iOS devices. + /// + /// Defaults to [Brightness.light]. final Brightness keyboardAppearance; + + /// The [ScrollPhysics] to use when vertically scrolling the input. + /// + /// This only has effect if [scrollable] is set to `true`. + /// + /// If not specified, it will behave according to the current platform. + /// + /// See [Scrollable.physics]. final ScrollPhysics? scrollPhysics; + + /// Callback to invoke when user wants to launch a URL. final ValueChanged? onLaunchUrl; + // Returns whether gesture is handled final bool Function( TapDownDetails details, TextPosition Function(Offset offset))? onTapDown; From 8813e888666457f4846986037fdfca5d9a92be01 Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 29 Dec 2021 18:47:11 -0800 Subject: [PATCH 176/179] Revert "Rename _resolvePadding() to resolvePadding()" This reverts commit f94b1382e109962180655c021e753b4146501873. --- lib/src/widgets/text_line.dart | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index ba119529..db0fc973 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -742,13 +742,11 @@ class RenderEditableTextLine extends RenderEditableBox { }).toList(growable: false); } - void resolvePadding() { + void _resolvePadding() { if (_resolvedPadding != null) { return; } _resolvedPadding = padding.resolve(textDirection); - _resolvedPadding = _resolvedPadding!.copyWith(left: _resolvedPadding!.left); - assert(_resolvedPadding!.isNonNegative); } @@ -950,7 +948,7 @@ class RenderEditableTextLine extends RenderEditableBox { @override double computeMinIntrinsicWidth(double height) { - resolvePadding(); + _resolvePadding(); final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; final leadingWidth = _leading == null @@ -966,7 +964,7 @@ class RenderEditableTextLine extends RenderEditableBox { @override double computeMaxIntrinsicWidth(double height) { - resolvePadding(); + _resolvePadding(); final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; final leadingWidth = _leading == null @@ -982,7 +980,7 @@ class RenderEditableTextLine extends RenderEditableBox { @override double computeMinIntrinsicHeight(double width) { - resolvePadding(); + _resolvePadding(); final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; if (_body != null) { @@ -995,7 +993,7 @@ class RenderEditableTextLine extends RenderEditableBox { @override double computeMaxIntrinsicHeight(double width) { - resolvePadding(); + _resolvePadding(); final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; if (_body != null) { @@ -1008,7 +1006,7 @@ class RenderEditableTextLine extends RenderEditableBox { @override double computeDistanceToActualBaseline(TextBaseline baseline) { - resolvePadding(); + _resolvePadding(); return _body!.getDistanceToActualBaseline(baseline)! + _resolvedPadding!.top; } @@ -1018,7 +1016,7 @@ class RenderEditableTextLine extends RenderEditableBox { final constraints = this.constraints; _selectedRects = null; - resolvePadding(); + _resolvePadding(); assert(_resolvedPadding != null); if (_body == null && _leading == null) { From 3fd8aa23dfdedc23aa51ebaf8026982ca08850ee Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 29 Dec 2021 18:55:36 -0800 Subject: [PATCH 177/179] Added maxContentWidth constraint to Editor --- lib/src/widgets/editor.dart | 89 ++++++++++++++++++++++++++++++--- lib/src/widgets/raw_editor.dart | 17 ++++++- 2 files changed, 97 insertions(+), 9 deletions(-) diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 4bcb1621..b0b4dfd2 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -236,6 +236,7 @@ class QuillEditor extends StatefulWidget { this.scrollBottomInset = 0, this.minHeight, this.maxHeight, + this.maxContentWidth, this.customStyles, this.textCapitalization = TextCapitalization.sentences, this.keyboardAppearance = Brightness.light, @@ -343,6 +344,14 @@ class QuillEditor extends StatefulWidget { /// This only has effect if [scrollable] is set to `true` and [expands] is /// set to `false`. final double? maxHeight; + + /// The maximum width to be occupied by the content of this editor. + /// + /// If this is not null and and this editor's width is larger than this value + /// then the contents will be constrained to the provided maximum width and + /// horizontally centered. This is mostly useful on devices with wide screens. + final double? maxContentWidth; + final DefaultStyles? customStyles; /// Whether this editor's height will be sized to fill its parent. @@ -519,6 +528,7 @@ class _QuillEditorState extends State textCapitalization: widget.textCapitalization, minHeight: widget.minHeight, maxHeight: widget.maxHeight, + maxContentWidth: widget.maxContentWidth, customStyles: widget.customStyles, expands: widget.expands, autoFocus: widget.autoFocus, @@ -828,11 +838,13 @@ class RenderEditor extends RenderEditableContainerBox List? children, EdgeInsets floatingCursorAddedMargin = const EdgeInsets.fromLTRB(4, 4, 4, 5), + double? maxContentWidth, }) : _hasFocus = hasFocus, _extendSelectionOrigin = selection, _startHandleLayerLink = startHandleLayerLink, _endHandleLayerLink = endHandleLayerLink, _cursorController = cursorController, + _maxContentWidth = maxContentWidth, super( children, document.root, @@ -967,6 +979,13 @@ class RenderEditor extends RenderEditableContainerBox markNeedsPaint(); } + double? _maxContentWidth; + set maxContentWidth(double? value) { + if (_maxContentWidth == value) return; + _maxContentWidth = value; + markNeedsLayout(); + } + @override List getEndpointsForSelection( TextSelection textSelection) { @@ -1206,6 +1225,60 @@ class RenderEditor extends RenderEditableContainerBox return TextSelection(baseOffset: line.start, extentOffset: line.end); } + @override + void performLayout() { + assert(() { + if (!constraints.hasBoundedHeight) return true; + throw FlutterError.fromParts([ + ErrorSummary('RenderEditableContainerBox must have ' + 'unlimited space along its main axis.'), + ErrorDescription('RenderEditableContainerBox does not clip or' + ' resize its children, so it must be ' + 'placed in a parent that does not constrain the main ' + 'axis.'), + ErrorHint( + 'You probably want to put the RenderEditableContainerBox inside a ' + 'RenderViewport with a matching main axis.') + ]); + }()); + assert(() { + if (constraints.hasBoundedWidth) return true; + throw FlutterError.fromParts([ + ErrorSummary('RenderEditableContainerBox must have a bounded' + ' constraint for its cross axis.'), + ErrorDescription('RenderEditableContainerBox forces its children to ' + "expand to fit the RenderEditableContainerBox's container, " + 'so it must be placed in a parent that constrains the cross ' + 'axis to a finite dimension.'), + ]); + }()); + + resolvePadding(); + assert(resolvedPadding != null); + + var mainAxisExtent = resolvedPadding!.top; + var child = firstChild; + final innerConstraints = BoxConstraints.tightFor( + width: math.min( + _maxContentWidth ?? double.infinity, constraints.maxWidth)) + .deflate(resolvedPadding!); + final leftOffset = _maxContentWidth == null + ? 0.0 + : math.max((constraints.maxWidth - _maxContentWidth!) / 2, 0); + while (child != null) { + child.layout(innerConstraints, parentUsesSize: true); + final childParentData = child.parentData as EditableContainerParentData + ..offset = Offset(resolvedPadding!.left + leftOffset, mainAxisExtent); + mainAxisExtent += child.size.height; + assert(child.parentData == childParentData); + child = childParentData.nextSibling; + } + mainAxisExtent += resolvedPadding!.bottom; + size = constraints.constrain(Size(constraints.maxWidth, mainAxisExtent)); + + assert(size.isFinite); + } + @override void paint(PaintingContext context, Offset offset) { if (_hasFocus && @@ -1628,7 +1701,7 @@ class RenderEditableContainerBox extends RenderBox EdgeInsets? get resolvedPadding => _resolvedPadding; - void _resolvePadding() { + void resolvePadding() { if (_resolvedPadding != null) { return; } @@ -1667,7 +1740,7 @@ class RenderEditableContainerBox extends RenderBox RenderEditableBox? childAtOffset(Offset offset) { assert(firstChild != null); - _resolvePadding(); + resolvePadding(); if (offset.dy <= _resolvedPadding!.top) { return firstChild; @@ -1701,7 +1774,7 @@ class RenderEditableContainerBox extends RenderBox @override void performLayout() { assert(constraints.hasBoundedWidth); - _resolvePadding(); + resolvePadding(); assert(_resolvedPadding != null); var mainAxisExtent = _resolvedPadding!.top; @@ -1747,7 +1820,7 @@ class RenderEditableContainerBox extends RenderBox @override double computeMinIntrinsicWidth(double height) { - _resolvePadding(); + resolvePadding(); return _getIntrinsicCrossAxis((child) { final childHeight = math.max( 0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); @@ -1759,7 +1832,7 @@ class RenderEditableContainerBox extends RenderBox @override double computeMaxIntrinsicWidth(double height) { - _resolvePadding(); + resolvePadding(); return _getIntrinsicCrossAxis((child) { final childHeight = math.max( 0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); @@ -1771,7 +1844,7 @@ class RenderEditableContainerBox extends RenderBox @override double computeMinIntrinsicHeight(double width) { - _resolvePadding(); + resolvePadding(); return _getIntrinsicMainAxis((child) { final childWidth = math.max( 0, width - _resolvedPadding!.left + _resolvedPadding!.right); @@ -1783,7 +1856,7 @@ class RenderEditableContainerBox extends RenderBox @override double computeMaxIntrinsicHeight(double width) { - _resolvePadding(); + resolvePadding(); return _getIntrinsicMainAxis((child) { final childWidth = math.max( 0, width - _resolvedPadding!.left + _resolvedPadding!.right); @@ -1795,7 +1868,7 @@ class RenderEditableContainerBox extends RenderBox @override double computeDistanceToActualBaseline(TextBaseline baseline) { - _resolvePadding(); + resolvePadding(); return defaultComputeDistanceToFirstActualBaseline(baseline)! + _resolvedPadding!.top; } diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 9a0b86dc..f77f1454 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -57,6 +57,7 @@ class RawEditor extends StatefulWidget { this.textCapitalization = TextCapitalization.none, this.maxHeight, this.minHeight, + this.maxContentWidth, this.customStyles, this.expands = false, this.autoFocus = false, @@ -152,6 +153,14 @@ class RawEditor extends StatefulWidget { /// The minimum height this editor can have. final double? minHeight; + + /// The maximum width to be occupied by the content of this editor. + /// + /// If this is not null and and this editor's width is larger than this value + /// then the contents will be constrained to the provided maximum width and + /// horizontally centered. This is mostly useful on devices with wide screens. + final double? maxContentWidth; + final DefaultStyles? customStyles; /// Whether this widget's height will be sized to fill its parent. @@ -281,6 +290,7 @@ class RawEditorState extends EditorState onSelectionChanged: _handleSelectionChanged, scrollBottomInset: widget.scrollBottomInset, padding: widget.padding, + maxContentWidth: widget.maxContentWidth, floatingCursorDisabled: widget.floatingCursorDisabled, children: _buildChildren(_doc, context), ), @@ -310,6 +320,7 @@ class RawEditorState extends EditorState onSelectionChanged: _handleSelectionChanged, scrollBottomInset: widget.scrollBottomInset, padding: widget.padding, + maxContentWidth: widget.maxContentWidth, cursorController: _cursorCont, floatingCursorDisabled: widget.floatingCursorDisabled, children: _buildChildren(_doc, context), @@ -907,6 +918,7 @@ class _Editor extends MultiChildRenderObjectWidget { required this.cursorController, required this.floatingCursorDisabled, this.padding = EdgeInsets.zero, + this.maxContentWidth, this.offset, }) : super(key: key, children: children); @@ -920,6 +932,7 @@ class _Editor extends MultiChildRenderObjectWidget { final TextSelectionChangedHandler onSelectionChanged; final double scrollBottomInset; final EdgeInsetsGeometry padding; + final double? maxContentWidth; final CursorCont cursorController; final bool floatingCursorDisabled; @@ -936,6 +949,7 @@ class _Editor extends MultiChildRenderObjectWidget { onSelectionChanged: onSelectionChanged, cursorController: cursorController, padding: padding, + maxContentWidth: maxContentWidth, scrollBottomInset: scrollBottomInset, floatingCursorDisabled: floatingCursorDisabled); } @@ -954,6 +968,7 @@ class _Editor extends MultiChildRenderObjectWidget { ..setEndHandleLayerLink(endHandleLayerLink) ..onSelectionChanged = onSelectionChanged ..setScrollBottomInset(scrollBottomInset) - ..setPadding(padding); + ..setPadding(padding) + ..maxContentWidth = maxContentWidth; } } From e4946ba4555d2e8638c9bab1c87cc24acb13d4de Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 29 Dec 2021 18:56:32 -0800 Subject: [PATCH 178/179] Upgrade to 3.0.4 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d08f4ebd..c372d239 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [3.0.4] +* Add maxContentWidth constraint to editor. + # [3.0.3] * Do not show caret on screen when the editor is not focused. diff --git a/pubspec.yaml b/pubspec.yaml index e3624607..abde4427 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: 3.0.3 +version: 3.0.4 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 0cb120d08d92c7091bfc0c9466a41c2b21965ca7 Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 3 Jan 2022 19:42:35 -0800 Subject: [PATCH 179/179] Code cleanup --- lib/src/models/documents/nodes/line.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/models/documents/nodes/line.dart b/lib/src/models/documents/nodes/line.dart index bccd7fb8..2476192a 100644 --- a/lib/src/models/documents/nodes/line.dart +++ b/lib/src/models/documents/nodes/line.dart @@ -359,8 +359,8 @@ class Line extends Container { result = result.mergeAll(node.style); var pos = node.length - data.offset; while (!node!.isLast && pos < local) { - node = node.next as Leaf?; - _handle(node!.style); + node = node.next as Leaf; + _handle(node.style); pos += node.length; } } @@ -391,8 +391,8 @@ class Line extends Container { result.add(node.style); var pos = node.length - data.offset; while (!node!.isLast && pos < local) { - node = node.next as Leaf?; - result.add(node!.style); + node = node.next as Leaf; + result.add(node.style); pos += node.length; } }