From e4cf28d054773d9c869c6829cbdef4bc17b0f180 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 22 Apr 2021 12:40:55 -0700 Subject: [PATCH 01/19] Update getIndentLevel --- lib/models/documents/attribute.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/models/documents/attribute.dart b/lib/models/documents/attribute.dart index 18822e01..09564e4e 100644 --- a/lib/models/documents/attribute.dart +++ b/lib/models/documents/attribute.dart @@ -151,7 +151,10 @@ class Attribute { if (level == 2) { return indentL2; } - return indentL3; + if (level == 3) { + return indentL3; + } + return IndentAttribute(level: level); } bool get isInline => scope == AttributeScope.INLINE; From 4c5f72826c0aee09cd8a0bade2b5ac8f990efdfc Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 22 Apr 2021 15:14:54 -0700 Subject: [PATCH 02/19] Indent attribute is consider block but may have null value --- lib/models/documents/attribute.dart | 32 +++++++++++++++++++++-------- lib/models/documents/style.dart | 3 ++- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/lib/models/documents/attribute.dart b/lib/models/documents/attribute.dart index 09564e4e..1b9043b9 100644 --- a/lib/models/documents/attribute.dart +++ b/lib/models/documents/attribute.dart @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:quiver/core.dart'; enum AttributeScope { @@ -14,7 +16,7 @@ class Attribute { final AttributeScope scope; final T value; - static final Map _registry = { + static final Map _registry = LinkedHashMap.of({ Attribute.bold.key: Attribute.bold, Attribute.italic.key: Attribute.italic, Attribute.underline.key: Attribute.underline, @@ -26,16 +28,16 @@ class Attribute { Attribute.background.key: Attribute.background, Attribute.placeholder.key: Attribute.placeholder, Attribute.header.key: Attribute.header, - Attribute.indent.key: Attribute.indent, Attribute.align.key: Attribute.align, Attribute.list.key: Attribute.list, Attribute.codeBlock.key: Attribute.codeBlock, Attribute.blockQuote.key: Attribute.blockQuote, + Attribute.indent.key: Attribute.indent, Attribute.width.key: Attribute.width, Attribute.height.key: Attribute.height, Attribute.style.key: Attribute.style, Attribute.token.key: Attribute.token, - }; + }); static final BoldAttribute bold = BoldAttribute(); @@ -88,22 +90,22 @@ class Attribute { Attribute.placeholder.key, }; - static final Set blockKeys = { + static final Set blockKeys = LinkedHashSet.of({ Attribute.header.key, - Attribute.indent.key, Attribute.align.key, Attribute.list.key, Attribute.codeBlock.key, Attribute.blockQuote.key, - }; + Attribute.indent.key, + }); - static final Set blockKeysExceptHeader = { + static final Set blockKeysExceptHeader = LinkedHashSet.of({ Attribute.list.key, - Attribute.indent.key, Attribute.align.key, Attribute.codeBlock.key, Attribute.blockQuote.key, - }; + Attribute.indent.key, + }); static Attribute get h1 => HeaderAttribute(level: 1); @@ -172,6 +174,18 @@ class Attribute { return attribute; } + static int getRegistryOrder(Attribute attribute) { + var order = 0; + for (final attr in _registry.values) { + if (attr.key == attribute.key) { + break; + } + order++; + } + + return order; + } + static Attribute clone(Attribute origin, dynamic value) { return Attribute(origin.key, origin.scope, value); } diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index c805280d..7f3a39ac 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -30,7 +30,8 @@ class Style { Iterable get keys => _attributes.keys; - Iterable get values => _attributes.values; + Iterable get values => _attributes.values.sorted( + (a, b) => Attribute.getRegistryOrder(a) - Attribute.getRegistryOrder(b)); Map get attributes => _attributes; From b6763fe2fcda949e28b8c429582aca0c4781801a Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 22 Apr 2021 16:10:46 -0700 Subject: [PATCH 03/19] Revert "Indent attribute is consider block but may have null value" This reverts commit 4c5f72826c0aee09cd8a0bade2b5ac8f990efdfc. --- lib/models/documents/attribute.dart | 32 ++++++++--------------------- lib/models/documents/style.dart | 3 +-- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/lib/models/documents/attribute.dart b/lib/models/documents/attribute.dart index 1b9043b9..09564e4e 100644 --- a/lib/models/documents/attribute.dart +++ b/lib/models/documents/attribute.dart @@ -1,5 +1,3 @@ -import 'dart:collection'; - import 'package:quiver/core.dart'; enum AttributeScope { @@ -16,7 +14,7 @@ class Attribute { final AttributeScope scope; final T value; - static final Map _registry = LinkedHashMap.of({ + static final Map _registry = { Attribute.bold.key: Attribute.bold, Attribute.italic.key: Attribute.italic, Attribute.underline.key: Attribute.underline, @@ -28,16 +26,16 @@ class Attribute { Attribute.background.key: Attribute.background, Attribute.placeholder.key: Attribute.placeholder, Attribute.header.key: Attribute.header, + Attribute.indent.key: Attribute.indent, Attribute.align.key: Attribute.align, Attribute.list.key: Attribute.list, Attribute.codeBlock.key: Attribute.codeBlock, Attribute.blockQuote.key: Attribute.blockQuote, - Attribute.indent.key: Attribute.indent, Attribute.width.key: Attribute.width, Attribute.height.key: Attribute.height, Attribute.style.key: Attribute.style, Attribute.token.key: Attribute.token, - }); + }; static final BoldAttribute bold = BoldAttribute(); @@ -90,22 +88,22 @@ class Attribute { Attribute.placeholder.key, }; - static final Set blockKeys = LinkedHashSet.of({ + static final Set blockKeys = { Attribute.header.key, + Attribute.indent.key, Attribute.align.key, Attribute.list.key, Attribute.codeBlock.key, Attribute.blockQuote.key, - Attribute.indent.key, - }); + }; - static final Set blockKeysExceptHeader = LinkedHashSet.of({ + static final Set blockKeysExceptHeader = { Attribute.list.key, + Attribute.indent.key, Attribute.align.key, Attribute.codeBlock.key, Attribute.blockQuote.key, - Attribute.indent.key, - }); + }; static Attribute get h1 => HeaderAttribute(level: 1); @@ -174,18 +172,6 @@ class Attribute { return attribute; } - static int getRegistryOrder(Attribute attribute) { - var order = 0; - for (final attr in _registry.values) { - if (attr.key == attribute.key) { - break; - } - order++; - } - - return order; - } - static Attribute clone(Attribute origin, dynamic value) { return Attribute(origin.key, origin.scope, value); } diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index 7f3a39ac..c805280d 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -30,8 +30,7 @@ class Style { Iterable get keys => _attributes.keys; - Iterable get values => _attributes.values.sorted( - (a, b) => Attribute.getRegistryOrder(a) - Attribute.getRegistryOrder(b)); + Iterable get values => _attributes.values; Map get attributes => _attributes; From bc5eb86a8f623bb2426a095835b2a72bb117ebd3 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 22 Apr 2021 16:25:35 -0700 Subject: [PATCH 04/19] Indent attribute is consider block but may have null value --- lib/models/documents/nodes/line.dart | 2 +- lib/models/documents/style.dart | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart index ec933b52..632d44d1 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -197,7 +197,7 @@ class Line extends Container { } applyStyle(newStyle); - final blockStyle = newStyle.getBlockExceptHeader(); + final blockStyle = newStyle.getNotNullValueBlockExceptHeader(); if (blockStyle == null) { return; } // No block-level changes diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index c805280d..4efce0f2 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -56,6 +56,15 @@ class Style { return null; } + Attribute? getNotNullValueBlockExceptHeader() { + for (final val in values) { + if (val.isBlockExceptHeader && val.value != null) { + return val; + } + } + return null; + } + Style merge(Attribute attribute) { final merged = Map.from(_attributes); if (attribute.value == null) { From df602cfa9b762d9c0248c081dbd5ddf308f7805f Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 22 Apr 2021 21:50:50 -0700 Subject: [PATCH 05/19] Format code --- lib/models/documents/nodes/line.dart | 2 +- lib/models/documents/style.dart | 8 +- lib/widgets/controller.dart | 9 +- lib/widgets/editor.dart | 32 ++- lib/widgets/raw_editor.dart | 6 +- lib/widgets/toolbar.dart | 377 ++++++++++++++------------- 6 files changed, 236 insertions(+), 198 deletions(-) diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart index 632d44d1..ec933b52 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -197,7 +197,7 @@ class Line extends Container { } applyStyle(newStyle); - final blockStyle = newStyle.getNotNullValueBlockExceptHeader(); + final blockStyle = newStyle.getBlockExceptHeader(); if (blockStyle == null) { return; } // No block-level changes diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index 4efce0f2..ff78d9ed 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -49,16 +49,12 @@ class Style { Attribute? getBlockExceptHeader() { for (final val in values) { - if (val.isBlockExceptHeader) { + if (val.isBlockExceptHeader && val.value != null) { return val; } } - return null; - } - - Attribute? getNotNullValueBlockExceptHeader() { for (final val in values) { - if (val.isBlockExceptHeader && val.value != null) { + if (val.isBlockExceptHeader) { return val; } } diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index 1b7a8180..59aa1e09 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -11,7 +11,11 @@ import '../models/quill_delta.dart'; import '../utils/diff_delta.dart'; class QuillController extends ChangeNotifier { - QuillController({required this.document, required this.selection, this.iconSize = 18, this.toolbarHeightFactor = 2}); + QuillController( + {required this.document, + required this.selection, + this.iconSize = 18, + this.toolbarHeightFactor = 2}); factory QuillController.basic() { return QuillController( @@ -80,7 +84,8 @@ class QuillController extends ChangeNotifier { bool get hasRedo => document.hasRedo; void replaceText( - int index, int len, Object? data, TextSelection? textSelection, {bool ignoreFocus = false}) { + int index, int len, Object? data, TextSelection? textSelection, + {bool ignoreFocus = false}) { assert(data is String || data is Embeddable); Delta? delta; diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 42fd769c..d15a8789 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -176,18 +176,25 @@ class QuillEditor extends StatefulWidget { final ScrollPhysics? scrollPhysics; final ValueChanged? onLaunchUrl; // Returns whether gesture is handled - final bool Function(TapDownDetails details, TextPosition Function(Offset offset))? onTapDown; + final bool Function( + TapDownDetails details, TextPosition Function(Offset offset))? onTapDown; // Returns whether gesture is handled - final bool Function(TapUpDetails details, TextPosition Function(Offset offset))? onTapUp; + final bool Function( + TapUpDetails details, TextPosition Function(Offset offset))? onTapUp; // Returns whether gesture is handled - final bool Function(LongPressStartDetails details, TextPosition Function(Offset offset))? onSingleLongTapStart; + final bool Function( + LongPressStartDetails details, TextPosition Function(Offset offset))? + onSingleLongTapStart; // Returns whether gesture is handled - final bool Function(LongPressMoveUpdateDetails details, TextPosition Function(Offset offset))? onSingleLongTapMoveUpdate; + final bool Function(LongPressMoveUpdateDetails details, + TextPosition Function(Offset offset))? onSingleLongTapMoveUpdate; // Returns whether gesture is handled - final bool Function(LongPressEndDetails details, TextPosition Function(Offset offset))? onSingleLongTapEnd; + final bool Function( + LongPressEndDetails details, TextPosition Function(Offset offset))? + onSingleLongTapEnd; final EmbedBuilder embedBuilder; @@ -339,7 +346,8 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onSingleLongTapMoveUpdate != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onSingleLongTapMoveUpdate!(details, renderEditor.getPositionForOffset)) { + if (_state.widget.onSingleLongTapMoveUpdate!( + details, renderEditor.getPositionForOffset)) { return; } } @@ -468,7 +476,8 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onTapDown != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onTapDown!(details, renderEditor.getPositionForOffset)) { + if (_state.widget.onTapDown!( + details, renderEditor.getPositionForOffset)) { return; } } @@ -481,7 +490,8 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onTapUp != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onTapUp!(details, renderEditor.getPositionForOffset)) { + if (_state.widget.onTapUp!( + details, renderEditor.getPositionForOffset)) { return; } } @@ -523,7 +533,8 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onSingleLongTapStart != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onSingleLongTapStart!(details, renderEditor.getPositionForOffset)) { + if (_state.widget.onSingleLongTapStart!( + details, renderEditor.getPositionForOffset)) { return; } } @@ -557,7 +568,8 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onSingleLongTapEnd != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onSingleLongTapEnd!(details, renderEditor.getPositionForOffset)) { + if (_state.widget.onSingleLongTapEnd!( + details, renderEditor.getPositionForOffset)) { return; } } diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 7114e670..35e3aa68 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -913,14 +913,16 @@ class RawEditorState extends EditorState return; } _showCaretOnScreen(); - _cursorCont.startOrStopCursorTimerIfNeeded(_hasFocus, widget.controller.selection); + _cursorCont.startOrStopCursorTimerIfNeeded( + _hasFocus, widget.controller.selection); if (hasConnection) { _cursorCont ..stopCursorTimer(resetCharTicks: false) ..startCursorTimer(); } - SchedulerBinding.instance!.addPostFrameCallback((_) => _updateOrDisposeSelectionOverlayIfNeeded()); + SchedulerBinding.instance!.addPostFrameCallback( + (_) => _updateOrDisposeSelectionOverlayIfNeeded()); if (mounted) { setState(() { // Use widget.controller.value in build() diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 8bcb3329..c5fe2bab 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -432,7 +432,8 @@ class _SelectHeaderStyleButtonState extends State { @override Widget build(BuildContext context) { - return _selectHeadingStyleButtonBuilder(context, _value, _selectAttribute, widget.controller.iconSize); + return _selectHeadingStyleButtonBuilder( + context, _value, _selectAttribute, widget.controller.iconSize); } } @@ -774,7 +775,8 @@ class _HistoryButtonState extends State { highlightElevation: 0, hoverElevation: 0, size: widget.controller.iconSize * 1.77, - icon: Icon(widget.icon, size: widget.controller.iconSize, color: _iconColor), + icon: Icon(widget.icon, + size: widget.controller.iconSize, color: _iconColor), fillColor: fillColor, onPressed: _changeHistory, ); @@ -839,7 +841,8 @@ class _IndentButtonState extends State { highlightElevation: 0, hoverElevation: 0, size: widget.controller.iconSize * 1.77, - icon: Icon(widget.icon, size: widget.controller.iconSize, color: iconColor), + icon: + Icon(widget.icon, size: widget.controller.iconSize, color: iconColor), fillColor: fillColor, onPressed: () { final indent = widget.controller @@ -893,7 +896,8 @@ class _ClearFormatButtonState extends State { highlightElevation: 0, hoverElevation: 0, size: widget.controller.iconSize * 1.77, - icon: Icon(widget.icon, size: widget.controller.iconSize, color: iconColor), + icon: Icon(widget.icon, + size: widget.controller.iconSize, color: iconColor), fillColor: fillColor, onPressed: () { for (final k @@ -905,7 +909,9 @@ class _ClearFormatButtonState extends State { } class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { - const QuillToolbar({required this.children, this.toolBarHeight = 36, Key? key}) : super(key: key); + const QuillToolbar( + {required this.children, this.toolBarHeight = 36, Key? key}) + : super(key: key); factory QuillToolbar.basic({ required QuillController controller, @@ -932,178 +938,195 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { }) { controller.iconSize = toolbarIconSize; - return QuillToolbar(key: key, toolBarHeight: toolbarIconSize * controller.toolbarHeightFactor, children: [ - Visibility( - visible: showHistory, - child: HistoryButton( - icon: Icons.undo_outlined, - controller: controller, - undo: true, - ), - ), - Visibility( - visible: showHistory, - child: HistoryButton( - icon: Icons.redo_outlined, - controller: controller, - undo: false, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showBoldButton, - child: ToggleStyleButton( - attribute: Attribute.bold, - icon: Icons.format_bold, - controller: controller, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showItalicButton, - child: ToggleStyleButton( - attribute: Attribute.italic, - icon: Icons.format_italic, - controller: controller, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showUnderLineButton, - child: ToggleStyleButton( - attribute: Attribute.underline, - icon: Icons.format_underline, - controller: controller, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showStrikeThrough, - child: ToggleStyleButton( - attribute: Attribute.strikeThrough, - icon: Icons.format_strikethrough, - controller: controller, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showColorButton, - child: ColorButton( - icon: Icons.color_lens, - controller: controller, - background: false, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showBackgroundColorButton, - child: ColorButton( - icon: Icons.format_color_fill, - controller: controller, - background: true, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showClearFormat, - child: ClearFormatButton( - icon: Icons.format_clear, - controller: controller, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: onImagePickCallback != null, - child: ImageButton( - icon: Icons.image, - controller: controller, - imageSource: ImageSource.gallery, - onImagePickCallback: onImagePickCallback, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: onImagePickCallback != null, - child: ImageButton( - icon: Icons.photo_camera, - controller: controller, - imageSource: ImageSource.camera, - onImagePickCallback: onImagePickCallback, - ), - ), - Visibility( - visible: showHeaderStyle, child: VerticalDivider(indent: 12, endIndent: 12, color: Colors.grey.shade400)), - Visibility(visible: showHeaderStyle, child: SelectHeaderStyleButton(controller: controller)), - VerticalDivider(indent: 12, endIndent: 12, color: Colors.grey.shade400), - Visibility( - visible: showListNumbers, - child: ToggleStyleButton( - attribute: Attribute.ol, - controller: controller, - icon: Icons.format_list_numbered, - ), - ), - Visibility( - visible: showListBullets, - child: ToggleStyleButton( - attribute: Attribute.ul, - controller: controller, - icon: Icons.format_list_bulleted, - ), - ), - Visibility( - visible: showListCheck, - child: ToggleCheckListButton( - attribute: Attribute.unchecked, - controller: controller, - icon: Icons.check_box, - ), - ), - Visibility( - visible: showCodeBlock, - child: ToggleStyleButton( - attribute: Attribute.codeBlock, - controller: controller, - icon: Icons.code, - ), - ), - Visibility( - visible: !showListNumbers && !showListBullets && !showListCheck && !showCodeBlock, - child: VerticalDivider(indent: 12, endIndent: 12, color: Colors.grey.shade400)), - Visibility( - visible: showQuote, - child: ToggleStyleButton( - attribute: Attribute.blockQuote, - controller: controller, - icon: Icons.format_quote, - ), - ), - Visibility( - visible: showIndent, - child: IndentButton( - icon: Icons.format_indent_increase, - controller: controller, - isIncrease: true, - ), - ), - Visibility( - visible: showIndent, - child: IndentButton( - icon: Icons.format_indent_decrease, - controller: controller, - isIncrease: false, - ), - ), - Visibility(visible: showQuote, child: VerticalDivider(indent: 12, endIndent: 12, color: Colors.grey.shade400)), - Visibility(visible: showLink, child: LinkStyleButton(controller: controller)), - Visibility( - visible: showHorizontalRule, - child: InsertEmbedButton( - controller: controller, - icon: Icons.horizontal_rule, - ), - ), - ]); + return QuillToolbar( + key: key, + toolBarHeight: toolbarIconSize * controller.toolbarHeightFactor, + children: [ + Visibility( + visible: showHistory, + child: HistoryButton( + icon: Icons.undo_outlined, + controller: controller, + undo: true, + ), + ), + Visibility( + visible: showHistory, + child: HistoryButton( + icon: Icons.redo_outlined, + controller: controller, + undo: false, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showBoldButton, + child: ToggleStyleButton( + attribute: Attribute.bold, + icon: Icons.format_bold, + controller: controller, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showItalicButton, + child: ToggleStyleButton( + attribute: Attribute.italic, + icon: Icons.format_italic, + controller: controller, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showUnderLineButton, + child: ToggleStyleButton( + attribute: Attribute.underline, + icon: Icons.format_underline, + controller: controller, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showStrikeThrough, + child: ToggleStyleButton( + attribute: Attribute.strikeThrough, + icon: Icons.format_strikethrough, + controller: controller, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showColorButton, + child: ColorButton( + icon: Icons.color_lens, + controller: controller, + background: false, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showBackgroundColorButton, + child: ColorButton( + icon: Icons.format_color_fill, + controller: controller, + background: true, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showClearFormat, + child: ClearFormatButton( + icon: Icons.format_clear, + controller: controller, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: onImagePickCallback != null, + child: ImageButton( + icon: Icons.image, + controller: controller, + imageSource: ImageSource.gallery, + onImagePickCallback: onImagePickCallback, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: onImagePickCallback != null, + child: ImageButton( + icon: Icons.photo_camera, + controller: controller, + imageSource: ImageSource.camera, + onImagePickCallback: onImagePickCallback, + ), + ), + Visibility( + visible: showHeaderStyle, + child: VerticalDivider( + indent: 12, endIndent: 12, color: Colors.grey.shade400)), + Visibility( + visible: showHeaderStyle, + child: SelectHeaderStyleButton(controller: controller)), + VerticalDivider( + indent: 12, endIndent: 12, color: Colors.grey.shade400), + Visibility( + visible: showListNumbers, + child: ToggleStyleButton( + attribute: Attribute.ol, + controller: controller, + icon: Icons.format_list_numbered, + ), + ), + Visibility( + visible: showListBullets, + child: ToggleStyleButton( + attribute: Attribute.ul, + controller: controller, + icon: Icons.format_list_bulleted, + ), + ), + Visibility( + visible: showListCheck, + child: ToggleCheckListButton( + attribute: Attribute.unchecked, + controller: controller, + icon: Icons.check_box, + ), + ), + Visibility( + visible: showCodeBlock, + child: ToggleStyleButton( + attribute: Attribute.codeBlock, + controller: controller, + icon: Icons.code, + ), + ), + Visibility( + visible: !showListNumbers && + !showListBullets && + !showListCheck && + !showCodeBlock, + child: VerticalDivider( + indent: 12, endIndent: 12, color: Colors.grey.shade400)), + Visibility( + visible: showQuote, + child: ToggleStyleButton( + attribute: Attribute.blockQuote, + controller: controller, + icon: Icons.format_quote, + ), + ), + Visibility( + visible: showIndent, + child: IndentButton( + icon: Icons.format_indent_increase, + controller: controller, + isIncrease: true, + ), + ), + Visibility( + visible: showIndent, + child: IndentButton( + icon: Icons.format_indent_decrease, + controller: controller, + isIncrease: false, + ), + ), + Visibility( + visible: showQuote, + child: VerticalDivider( + indent: 12, endIndent: 12, color: Colors.grey.shade400)), + Visibility( + visible: showLink, + child: LinkStyleButton(controller: controller)), + Visibility( + visible: showHorizontalRule, + child: InsertEmbedButton( + controller: controller, + icon: Icons.horizontal_rule, + ), + ), + ]); } final List children; From 80d918ef9ca6c39c1c38f8727dbf826cb64b7017 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 22 Apr 2021 22:20:54 -0700 Subject: [PATCH 06/19] Update PreserveBlockStyleOnInsertRule to apply all block styles --- lib/models/documents/style.dart | 10 ++++++++++ lib/models/rules/insert.dart | 17 ++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index ff78d9ed..8174aeca 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -61,6 +61,16 @@ class Style { return null; } + Map getBlocksExceptHeader() { + final m = {}; + attributes.forEach((key, value) { + if (Attribute.blockKeysExceptHeader.contains(key)) { + m[key] = value; + } + }); + return m; + } + Style merge(Attribute attribute) { final merged = Map.from(_attributes); if (attribute.value == null) { diff --git a/lib/models/rules/insert.dart b/lib/models/rules/insert.dart index b83c8838..cc7a3a58 100644 --- a/lib/models/rules/insert.dart +++ b/lib/models/rules/insert.dart @@ -62,28 +62,32 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { Delta? applyRule(Delta document, int index, {int? len, Object? data, Attribute? attribute}) { if (data is! String || !data.contains('\n')) { + // Only interested in text containing at least one newline character. return null; } final itr = DeltaIterator(document)..skip(index); + // Look for the next newline. final nextNewLine = _getNextNewLine(itr); final lineStyle = Style.fromJson(nextNewLine.item1?.attributes ?? {}); - final attribute = lineStyle.getBlockExceptHeader(); - if (attribute == null) { + final blockStyle = lineStyle.getBlocksExceptHeader(); + // Are we currently in a block? If not then ignore. + if (blockStyle.isEmpty) { return null; } - final blockStyle = {attribute.key: attribute.value}; - Map? 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(); } + // Go over each inserted line and ensure block style is applied. final lines = data.split('\n'); final delta = Delta()..retain(index + (len ?? 0)); for (var i = 0; i < lines.length; i++) { @@ -92,12 +96,15 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { delta.insert(line); } if (i == 0) { + // The first line should inherit the lineStyle entirely. delta.insert('\n', lineStyle.toJson()); } else if (i < lines.length - 1) { + // we don't want to insert a newline after the last chunk of text, so -1 delta.insert('\n', blockStyle); } } + // Reset style of the original newline character if needed. if (resetStyle != null) { delta ..retain(nextNewLine.item2!) From f7d47a12db381d1799e7e886e6634a78cd075b64 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 22 Apr 2021 22:26:59 -0700 Subject: [PATCH 07/19] Add comments to AutoExitBlockRule --- lib/models/rules/insert.dart | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lib/models/rules/insert.dart b/lib/models/rules/insert.dart index cc7a3a58..5801a10e 100644 --- a/lib/models/rules/insert.dart +++ b/lib/models/rules/insert.dart @@ -55,6 +55,14 @@ class PreserveLineStyleOnSplitRule extends InsertRule { } } +/// Preserves block style when user inserts text containing newlines. +/// +/// This rule handles: +/// +/// * inserting a new line in a block +/// * pasting text containing multiple lines of text in a block +/// +/// This rule may also be activated for changes triggered by auto-correct. class PreserveBlockStyleOnInsertRule extends InsertRule { const PreserveBlockStyleOnInsertRule(); @@ -116,6 +124,12 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { } } +/// Heuristic rule to exit current block when user inserts two consecutive +/// newlines. +/// +/// This rule is only applied when the cursor is on the last line of a block. +/// When the cursor is in the middle of a block we allow adding empty lines +/// and preserving the block's style. class AutoExitBlockRule extends InsertRule { const AutoExitBlockRule(); @@ -139,25 +153,39 @@ class AutoExitBlockRule extends InsertRule { final itr = DeltaIterator(document); final prev = itr.skip(index), cur = itr.next(); final blockStyle = Style.fromJson(cur.attributes).getBlockExceptHeader(); + // We are not in a block, ignore. if (cur.isPlain || blockStyle == null) { return null; } + // We are not on an empty line, ignore. if (!_isEmptyLine(prev, cur)) { return null; } + // We are on an empty line. Now we need to determine if we are on the + // last line of a block. + // First check if `cur` length is greater than 1, this would indicate + // that it contains multiple newline characters which share the same style. + // This would mean we are not on the last line yet. + // `cur.value as String` is safe since we already called isEmptyLine and know it contains a newline if ((cur.value as String).length > 1) { + // We are not on the last line of this block, ignore. return null; } + // Keep looking for the next newline character to see if it shares the same + // block style as `cur`. final nextNewLine = _getNextNewLine(itr); if (nextNewLine.item1 != null && nextNewLine.item1!.attributes != null && Style.fromJson(nextNewLine.item1!.attributes).getBlockExceptHeader() == blockStyle) { + // We are not at the end of this block, ignore. return null; } + // Here we now know that the line after `cur` is not in the same block + // therefore we can exit this block. final attributes = cur.attributes ?? {}; final k = attributes.keys .firstWhere((k) => Attribute.blockKeysExceptHeader.contains(k)); From 900d0f2489ed2c731b52ae8594af80da617df1a4 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 22 Apr 2021 22:40:01 -0700 Subject: [PATCH 08/19] Fix: Indented position not holding while editing --- CHANGELOG.md | 3 +++ lib/models/documents/nodes/line.dart | 17 +++++++++++------ pubspec.yaml | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8937de42..d96c8386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.2.1] +* Indented position not holding while editing. + ## [1.2.0] * Fix image button cancel causes crash. diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart index ec933b52..c34a8ed8 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -208,18 +208,23 @@ class Line extends Container { _unwrap(); } else if (blockStyle != parentStyle) { _unwrap(); - final block = Block()..applyAttribute(blockStyle); - _wrap(block); - block.adjust(); + _applyBlockStyles(newStyle); } // else the same style, no-op. } else if (blockStyle.value != null) { // Only wrap with a new block if this is not an unset - final block = Block()..applyAttribute(blockStyle); - _wrap(block); - block.adjust(); + _applyBlockStyles(newStyle); } } + void _applyBlockStyles(Style newStyle) { + var block = Block(); + for (final style in newStyle.getBlocksExceptHeader().values) { + block = block..applyAttribute(style); + } + _wrap(block); + block.adjust(); + } + /// Wraps this line with new parent [block]. /// /// This line can not be in a [Block] when this method is called. diff --git a/pubspec.yaml b/pubspec.yaml index 93bf71be..5e87f6d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 1.2.0 +version: 1.2.1 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 7cdbdd9a6a10e1f5bdbaf22955b5bc59ac52b8d0 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 22 Apr 2021 23:56:53 -0700 Subject: [PATCH 09/19] Make attribute registry ordered --- lib/models/documents/attribute.dart | 32 +++++++++++++++++++++-------- lib/models/documents/style.dart | 3 ++- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/lib/models/documents/attribute.dart b/lib/models/documents/attribute.dart index 09564e4e..1b9043b9 100644 --- a/lib/models/documents/attribute.dart +++ b/lib/models/documents/attribute.dart @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:quiver/core.dart'; enum AttributeScope { @@ -14,7 +16,7 @@ class Attribute { final AttributeScope scope; final T value; - static final Map _registry = { + static final Map _registry = LinkedHashMap.of({ Attribute.bold.key: Attribute.bold, Attribute.italic.key: Attribute.italic, Attribute.underline.key: Attribute.underline, @@ -26,16 +28,16 @@ class Attribute { Attribute.background.key: Attribute.background, Attribute.placeholder.key: Attribute.placeholder, Attribute.header.key: Attribute.header, - Attribute.indent.key: Attribute.indent, Attribute.align.key: Attribute.align, Attribute.list.key: Attribute.list, Attribute.codeBlock.key: Attribute.codeBlock, Attribute.blockQuote.key: Attribute.blockQuote, + Attribute.indent.key: Attribute.indent, Attribute.width.key: Attribute.width, Attribute.height.key: Attribute.height, Attribute.style.key: Attribute.style, Attribute.token.key: Attribute.token, - }; + }); static final BoldAttribute bold = BoldAttribute(); @@ -88,22 +90,22 @@ class Attribute { Attribute.placeholder.key, }; - static final Set blockKeys = { + static final Set blockKeys = LinkedHashSet.of({ Attribute.header.key, - Attribute.indent.key, Attribute.align.key, Attribute.list.key, Attribute.codeBlock.key, Attribute.blockQuote.key, - }; + Attribute.indent.key, + }); - static final Set blockKeysExceptHeader = { + static final Set blockKeysExceptHeader = LinkedHashSet.of({ Attribute.list.key, - Attribute.indent.key, Attribute.align.key, Attribute.codeBlock.key, Attribute.blockQuote.key, - }; + Attribute.indent.key, + }); static Attribute get h1 => HeaderAttribute(level: 1); @@ -172,6 +174,18 @@ class Attribute { return attribute; } + static int getRegistryOrder(Attribute attribute) { + var order = 0; + for (final attr in _registry.values) { + if (attr.key == attribute.key) { + break; + } + order++; + } + + return order; + } + static Attribute clone(Attribute origin, dynamic value) { return Attribute(origin.key, origin.scope, value); } diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index 8174aeca..fade1bb5 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -30,7 +30,8 @@ class Style { Iterable get keys => _attributes.keys; - Iterable get values => _attributes.values; + Iterable get values => _attributes.values.sorted( + (a, b) => Attribute.getRegistryOrder(a) - Attribute.getRegistryOrder(b)); Map get attributes => _attributes; From 3e576aeb9a0bb76e7da9e2cc67b1204a78bd4664 Mon Sep 17 00:00:00 2001 From: "Guilherme C. Matuella" Date: Fri, 23 Apr 2021 18:34:16 -0300 Subject: [PATCH 10/19] Fixes `Document.toJson` to map from raw operations (#162) --- lib/models/quill_delta.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/quill_delta.dart b/lib/models/quill_delta.dart index 895b27fe..a0e608be 100644 --- a/lib/models/quill_delta.dart +++ b/lib/models/quill_delta.dart @@ -265,7 +265,7 @@ class Delta { List toList() => List.from(_operations); /// Returns JSON-serializable version of this delta. - List toJson() => toList(); + List toJson() => toList().map((operation) => operation.toJson()).toList(); /// Returns `true` if this delta is empty. bool get isEmpty => _operations.isEmpty; From 60127aeb048730e32a8b31d2250fc3f07c411bbb Mon Sep 17 00:00:00 2001 From: "Guilherme C. Matuella" Date: Fri, 23 Apr 2021 18:44:02 -0300 Subject: [PATCH 11/19] Adds `doc.isEmpty` check to `Document._loadDocument(Delta doc)` (#163) --- lib/models/documents/document.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index 83f7a0fc..68dbee4d 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -223,7 +223,12 @@ class Document { String toPlainText() => _root.children.map((e) => e.toPlainText()).join(); void _loadDocument(Delta doc) { + if (doc.isEmpty) { + throw ArgumentError.value(doc, 'Document Delta cannot be empty.'); + } + assert((doc.last.data as String).endsWith('\n')); + var offset = 0; for (final op in doc.toList()) { if (!op.isInsert) { From 451dffc4cdb96ec4a851cb2e7b0f8bb2f70f626a Mon Sep 17 00:00:00 2001 From: "Guilherme C. Matuella" Date: Fri, 23 Apr 2021 19:14:33 -0300 Subject: [PATCH 12/19] Fixes crashing disposed listeners on a bunch of widgets (#164) --- lib/widgets/controller.dart | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index 59aa1e09..a0c2aa78 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -32,6 +32,13 @@ class QuillController extends ChangeNotifier { Style toggledStyle = Style(); bool ignoreFocusOnTextChange = false; + /// Controls whether this [QuillController] instance has already been disposed + /// of + /// + /// This is a safe approach to make sure that listeners don't crash when + /// adding, removing or listeners to this instance. + bool _isDisposed = false; + // item1: Document state before [change]. // // item2: Change delta applied to the document. @@ -183,9 +190,31 @@ class QuillController extends ChangeNotifier { notifyListeners(); } + @override + void addListener(VoidCallback listener) { + // By using `_isDisposed`, make sure that `addListener` won't be called on a + // disposed `ChangeListener` + if (!_isDisposed) { + super.addListener(listener); + } + } + + @override + void removeListener(VoidCallback listener) { + // By using `_isDisposed`, make sure that `removeListener` won't be called + // on a disposed `ChangeListener` + if (!_isDisposed) { + super.removeListener(listener); + } + } + @override void dispose() { - document.close(); + if (!_isDisposed) { + document.close(); + } + + _isDisposed = true; super.dispose(); } From ca9a13b1505589c8df104a78f2b6d8a36bb32ff4 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 23 Apr 2021 16:23:10 -0700 Subject: [PATCH 13/19] Update Line _format method --- lib/models/documents/nodes/line.dart | 7 +++++-- lib/widgets/controller.dart | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart index c34a8ed8..fabfad4d 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -1,5 +1,7 @@ import 'dart:math' as math; +import 'package:collection/collection.dart'; + import '../../quill_delta.dart'; import '../attribute.dart'; import '../style.dart'; @@ -203,10 +205,11 @@ class Line extends Container { } // No block-level changes if (parent is Block) { - final parentStyle = (parent as Block).style.getBlockExceptHeader(); + final parentStyle = (parent as Block).style.getBlocksExceptHeader(); if (blockStyle.value == null) { _unwrap(); - } else if (blockStyle != parentStyle) { + } else if (!const MapEquality() + .equals(newStyle.getBlocksExceptHeader(), parentStyle)) { _unwrap(); _applyBlockStyles(newStyle); } // else the same style, no-op. diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index a0c2aa78..cb5136df 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -34,7 +34,7 @@ class QuillController extends ChangeNotifier { /// Controls whether this [QuillController] instance has already been disposed /// of - /// + /// /// This is a safe approach to make sure that listeners don't crash when /// adding, removing or listeners to this instance. bool _isDisposed = false; From da2a05aaa0baef057750426f5b4ca749060c2be2 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Mon, 26 Apr 2021 00:45:40 -0700 Subject: [PATCH 14/19] Update issue templates --- .github/ISSUE_TEMPLATE/issue-template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/issue-template.md b/.github/ISSUE_TEMPLATE/issue-template.md index ff759a4b..077b59c0 100644 --- a/.github/ISSUE_TEMPLATE/issue-template.md +++ b/.github/ISSUE_TEMPLATE/issue-template.md @@ -13,4 +13,4 @@ My issue is about [Desktop] I have tried running `example` directory successfully before creating an issue here. -Please note that we are using stable channel. If you are using beta or master channel, those are not supported. +Please note that we are using stable channel on branch master. If you are using beta or master channel, use branch dev. From e49421f48c8dbd30b40ed056c8edd5ab24dc5688 Mon Sep 17 00:00:00 2001 From: kevinDespoulains <46108869+kevinDespoulains@users.noreply.github.com> Date: Mon, 26 Apr 2021 17:50:46 +0200 Subject: [PATCH 15/19] Updating checkbox to handle tap (#186) * updating checkbox to handle tap * updating checkbox to handle long press and using UniqueKey() to avoid weird side effects * removed useless doc Co-authored-by: Kevin Despoulains --- lib/widgets/editor.dart | 32 +---------------- lib/widgets/raw_editor.dart | 44 +++++++++++++++-------- lib/widgets/text_block.dart | 70 ++++++++++++++++++++----------------- 3 files changed, 68 insertions(+), 78 deletions(-) diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index d15a8789..f6dd8e40 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -393,8 +393,6 @@ class _QuillEditorSelectionGestureDetectorBuilder final segmentResult = line.queryChild(result.offset, false); if (segmentResult.node == null) { if (line.length == 1) { - // tapping when no text yet on this line - _flipListCheckbox(pos, line, segmentResult); getEditor()!.widget.controller.updateSelection( TextSelection.collapsed(offset: pos.offset), ChangeSource.LOCAL); return true; @@ -434,37 +432,9 @@ class _QuillEditorSelectionGestureDetectorBuilder ), ); } - return false; - } - if (_flipListCheckbox(pos, line, segmentResult)) { - return true; } - return false; - } - bool _flipListCheckbox( - TextPosition pos, Line line, container_node.ChildQuery segmentResult) { - if (getEditor()!.widget.readOnly || - !line.style.containsKey(Attribute.list.key) || - segmentResult.offset != 0) { - return false; - } - // segmentResult.offset == 0 means tap at the beginning of the TextLine - final String? listVal = line.style.attributes[Attribute.list.key]!.value; - if (listVal == Attribute.unchecked.value) { - getEditor()! - .widget - .controller - .formatText(pos.offset, 0, Attribute.checked); - } else if (listVal == Attribute.checked.value) { - getEditor()! - .widget - .controller - .formatText(pos.offset, 0, Attribute.unchecked); - } - getEditor()!.widget.controller.updateSelection( - TextSelection.collapsed(offset: pos.offset), ChangeSource.LOCAL); - return true; + return false; } Future _launchUrl(String url) async { diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 35e3aa68..f93eabe2 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -579,6 +579,18 @@ class RawEditorState extends EditorState } } + /// Updates the checkbox positioned at [offset] in document + /// by changing its attribute according to [value]. + void _handleCheckboxTap(int offset, bool value) { + if (!widget.readOnly) { + if (value) { + widget.controller.formatText(offset, 0, Attribute.checked); + } else { + widget.controller.formatText(offset, 0, Attribute.unchecked); + } + } + } + List _buildChildren(Document doc, BuildContext context) { final result = []; final indentLevelCounts = {}; @@ -589,21 +601,23 @@ class RawEditorState extends EditorState } else if (node is Block) { final attrs = node.style.attributes; final editableTextBlock = EditableTextBlock( - node, - _textDirection, - widget.scrollBottomInset, - _getVerticalSpacingForBlock(node, _styles), - widget.controller.selection, - widget.selectionColor, - _styles, - widget.enableInteractiveSelection, - _hasFocus, - attrs.containsKey(Attribute.codeBlock.key) - ? const EdgeInsets.all(16) - : null, - widget.embedBuilder, - _cursorCont, - indentLevelCounts); + node, + _textDirection, + widget.scrollBottomInset, + _getVerticalSpacingForBlock(node, _styles), + widget.controller.selection, + widget.selectionColor, + _styles, + widget.enableInteractiveSelection, + _hasFocus, + attrs.containsKey(Attribute.codeBlock.key) + ? const EdgeInsets.all(16) + : null, + widget.embedBuilder, + _cursorCont, + indentLevelCounts, + _handleCheckboxTap, + ); result.add(editableTextBlock); } else { throw StateError('Unreachable.'); diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index 309b9cf8..f533a160 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -61,6 +61,7 @@ class EditableTextBlock extends StatelessWidget { this.embedBuilder, this.cursorCont, this.indentLevelCounts, + this.onCheckboxTap, ); final Block block; @@ -76,6 +77,7 @@ class EditableTextBlock extends StatelessWidget { final EmbedBuilder embedBuilder; final CursorCont cursorCont; final Map indentLevelCounts; + final Function(int, bool) onCheckboxTap; @override Widget build(BuildContext context) { @@ -161,12 +163,23 @@ class EditableTextBlock extends StatelessWidget { if (attrs[Attribute.list.key] == Attribute.checked) { return _Checkbox( - style: defaultStyles!.leading!.style, width: 32, isChecked: true); + key: UniqueKey(), + style: defaultStyles!.leading!.style, + width: 32, + isChecked: true, + offset: block.offset + line.offset, + onTap: onCheckboxTap, + ); } if (attrs[Attribute.list.key] == Attribute.unchecked) { return _Checkbox( - style: defaultStyles!.leading!.style, width: 32, isChecked: false); + key: UniqueKey(), + style: defaultStyles!.leading!.style, + width: 32, + offset: block.offset + line.offset, + onTap: onCheckboxTap, + ); } if (attrs.containsKey(Attribute.codeBlock.key)) { @@ -685,46 +698,39 @@ class _BulletPoint extends StatelessWidget { } } -class _Checkbox extends StatefulWidget { - const _Checkbox({Key? key, this.style, this.width, this.isChecked}) - : super(key: key); - +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 bool isChecked; + final int? offset; + final Function(int, bool)? onTap; - @override - __CheckboxState createState() => __CheckboxState(); -} - -class __CheckboxState extends State<_Checkbox> { - bool? isChecked; - - void _onCheckboxClicked(bool? newValue) => setState(() { - isChecked = newValue; - - if (isChecked!) { - // check list - } else { - // uncheck list - } - }); - - @override - void initState() { - super.initState(); - isChecked = widget.isChecked; + 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: widget.width, + width: width, padding: const EdgeInsetsDirectional.only(end: 13), - child: Checkbox( - value: widget.isChecked, - onChanged: _onCheckboxClicked, + child: GestureDetector( + onLongPress: () => _onCheckboxClicked(!isChecked), + child: Checkbox( + value: isChecked, + onChanged: _onCheckboxClicked, + ), ), ); } From 760b4def7ea0c4ef39857572d16e329aad3f6dfb Mon Sep 17 00:00:00 2001 From: em6m6e <50019687+em6m6e@users.noreply.github.com> Date: Mon, 26 Apr 2021 17:52:37 +0200 Subject: [PATCH 16/19] Simple viewer (#187) * 2021-04-25 * 2021-04-26 --- lib/widgets/simple_viewer.dart | 337 +++++++++++++++++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 lib/widgets/simple_viewer.dart diff --git a/lib/widgets/simple_viewer.dart b/lib/widgets/simple_viewer.dart new file mode 100644 index 00000000..97cdcedd --- /dev/null +++ b/lib/widgets/simple_viewer.dart @@ -0,0 +1,337 @@ +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:string_validator/string_validator.dart'; +import 'package:tuple/tuple.dart'; + +import '../models/documents/attribute.dart'; +import '../models/documents/document.dart'; +import '../models/documents/nodes/block.dart'; +import '../models/documents/nodes/leaf.dart' as leaf; +import '../models/documents/nodes/line.dart'; +import 'controller.dart'; +import 'cursor.dart'; +import 'default_styles.dart'; +import 'delegate.dart'; +import 'editor.dart'; +import 'text_block.dart'; +import 'text_line.dart'; + +class QuillSimpleViewer extends StatefulWidget { + const QuillSimpleViewer({ + required this.controller, + this.customStyles, + this.truncate = false, + this.truncateScale, + this.truncateAlignment, + this.truncateHeight, + this.truncateWidth, + this.scrollBottomInset = 0, + this.padding = EdgeInsets.zero, + this.embedBuilder, + Key? key, + }) : assert(truncate || + ((truncateScale == null) && + (truncateAlignment == null) && + (truncateHeight == null) && + (truncateWidth == null))), + super(key: key); + + final QuillController controller; + final DefaultStyles? customStyles; + final bool truncate; + final double? truncateScale; + final Alignment? truncateAlignment; + final double? truncateHeight; + final double? truncateWidth; + final double scrollBottomInset; + final EdgeInsetsGeometry padding; + final EmbedBuilder? embedBuilder; + + @override + _QuillSimpleViewerState createState() => _QuillSimpleViewerState(); +} + +class _QuillSimpleViewerState extends State + with SingleTickerProviderStateMixin { + late DefaultStyles _styles; + final LayerLink _toolbarLayerLink = LayerLink(); + final LayerLink _startHandleLayerLink = LayerLink(); + final LayerLink _endHandleLayerLink = LayerLink(); + late CursorCont _cursorCont; + + @override + void initState() { + super.initState(); + + _cursorCont = CursorCont( + show: ValueNotifier(false), + style: const CursorStyle( + color: Colors.black, + backgroundColor: Colors.grey, + width: 2, + radius: Radius.zero, + offset: Offset.zero, + ), + tickerProvider: this, + ); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final parentStyles = QuillStyles.getStyles(context, true); + final defaultStyles = DefaultStyles.getInstance(context); + _styles = (parentStyles != null) + ? defaultStyles.merge(parentStyles) + : defaultStyles; + + if (widget.customStyles != null) { + _styles = _styles.merge(widget.customStyles!); + } + } + + EmbedBuilder get embedBuilder => widget.embedBuilder ?? _defaultEmbedBuilder; + + Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) { + assert(!kIsWeb, 'Please provide EmbedBuilder for Web'); + switch (node.value.type) { + case 'image': + final imageUrl = _standardizeImageUrl(node.value.data); + return imageUrl.startsWith('http') + ? Image.network(imageUrl) + : isBase64(imageUrl) + ? Image.memory(base64.decode(imageUrl)) + : Image.file(io.File(imageUrl)); + default: + throw UnimplementedError( + 'Embeddable type "${node.value.type}" is not supported by default embed ' + 'builder of QuillEditor. You must pass your own builder function to ' + 'embedBuilder property of QuillEditor or QuillField widgets.'); + } + } + + String _standardizeImageUrl(String url) { + if (url.contains('base64')) { + return url.split(',')[1]; + } + return url; + } + + @override + Widget build(BuildContext context) { + final _doc = widget.controller.document; + // if (_doc.isEmpty() && + // !widget.focusNode.hasFocus && + // widget.placeholder != null) { + // _doc = Document.fromJson(jsonDecode( + // '[{"attributes":{"placeholder":true},"insert":"${widget.placeholder}\\n"}]')); + // } + + Widget child = CompositedTransformTarget( + link: _toolbarLayerLink, + child: Semantics( + child: _SimpleViewer( + document: _doc, + textDirection: _textDirection, + startHandleLayerLink: _startHandleLayerLink, + endHandleLayerLink: _endHandleLayerLink, + onSelectionChanged: _nullSelectionChanged, + scrollBottomInset: widget.scrollBottomInset, + padding: widget.padding, + children: _buildChildren(_doc, context), + ), + ), + ); + + if (widget.truncate) { + if (widget.truncateScale != null) { + child = Container( + height: widget.truncateHeight, + child: Align( + heightFactor: widget.truncateScale, + widthFactor: widget.truncateScale, + alignment: widget.truncateAlignment ?? Alignment.topLeft, + child: Container( + width: widget.truncateWidth! / widget.truncateScale!, + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Transform.scale( + scale: widget.truncateScale!, + alignment: + widget.truncateAlignment ?? Alignment.topLeft, + child: child))))); + } else { + child = Container( + height: widget.truncateHeight, + width: widget.truncateWidth, + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), child: child)); + } + } + + return QuillStyles(data: _styles, child: child); + } + + List _buildChildren(Document doc, BuildContext context) { + final result = []; + final indentLevelCounts = {}; + for (final node in doc.root.children) { + if (node is Line) { + final editableTextLine = _getEditableTextLineFromNode(node, context); + result.add(editableTextLine); + } else if (node is Block) { + final attrs = node.style.attributes; + final editableTextBlock = EditableTextBlock( + node, + _textDirection, + widget.scrollBottomInset, + _getVerticalSpacingForBlock(node, _styles), + widget.controller.selection, + Colors.black, + // selectionColor, + _styles, + false, + // enableInteractiveSelection, + false, + // hasFocus, + attrs.containsKey(Attribute.codeBlock.key) + ? const EdgeInsets.all(16) + : null, + embedBuilder, + _cursorCont, + indentLevelCounts); + result.add(editableTextBlock); + } else { + throw StateError('Unreachable.'); + } + } + return result; + } + + TextDirection get _textDirection { + final result = Directionality.of(context); + return result; + } + + EditableTextLine _getEditableTextLineFromNode( + Line node, BuildContext context) { + final textLine = TextLine( + line: node, + textDirection: _textDirection, + embedBuilder: embedBuilder, + styles: _styles, + ); + final editableTextLine = EditableTextLine( + node, + null, + textLine, + 0, + _getVerticalSpacingForLine(node, _styles), + _textDirection, + widget.controller.selection, + Colors.black, + //widget.selectionColor, + false, + //enableInteractiveSelection, + false, + //_hasFocus, + MediaQuery.of(context).devicePixelRatio, + _cursorCont); + return editableTextLine; + } + + Tuple2 _getVerticalSpacingForLine( + Line line, DefaultStyles? defaultStyles) { + final attrs = line.style.attributes; + if (attrs.containsKey(Attribute.header.key)) { + final int? level = attrs[Attribute.header.key]!.value; + switch (level) { + case 1: + return defaultStyles!.h1!.verticalSpacing; + case 2: + return defaultStyles!.h2!.verticalSpacing; + case 3: + return defaultStyles!.h3!.verticalSpacing; + default: + throw 'Invalid level $level'; + } + } + + return defaultStyles!.paragraph!.verticalSpacing; + } + + Tuple2 _getVerticalSpacingForBlock( + Block node, DefaultStyles? defaultStyles) { + final attrs = node.style.attributes; + if (attrs.containsKey(Attribute.blockQuote.key)) { + return defaultStyles!.quote!.verticalSpacing; + } else if (attrs.containsKey(Attribute.codeBlock.key)) { + return defaultStyles!.code!.verticalSpacing; + } else if (attrs.containsKey(Attribute.indent.key)) { + return defaultStyles!.indent!.verticalSpacing; + } + return defaultStyles!.lists!.verticalSpacing; + } + + void _nullSelectionChanged( + TextSelection selection, SelectionChangedCause cause) {} +} + +class _SimpleViewer extends MultiChildRenderObjectWidget { + _SimpleViewer({ + required List children, + required this.document, + required this.textDirection, + required this.startHandleLayerLink, + required this.endHandleLayerLink, + required this.onSelectionChanged, + required this.scrollBottomInset, + this.padding = EdgeInsets.zero, + Key? key, + }) : super(key: key, children: children); + + final Document document; + final TextDirection textDirection; + final LayerLink startHandleLayerLink; + final LayerLink endHandleLayerLink; + final TextSelectionChangedHandler onSelectionChanged; + final double scrollBottomInset; + final EdgeInsetsGeometry padding; + + @override + RenderEditor createRenderObject(BuildContext context) { + return RenderEditor( + null, + textDirection, + scrollBottomInset, + padding, + document, + const TextSelection(baseOffset: 0, extentOffset: 0), + false, + // hasFocus, + onSelectionChanged, + startHandleLayerLink, + endHandleLayerLink, + const EdgeInsets.fromLTRB(4, 4, 4, 5), + ); + } + + @override + void updateRenderObject( + BuildContext context, covariant RenderEditor renderObject) { + renderObject + ..document = document + ..setContainer(document.root) + ..textDirection = textDirection + ..setStartHandleLayerLink(startHandleLayerLink) + ..setEndHandleLayerLink(endHandleLayerLink) + ..onSelectionChanged = onSelectionChanged + ..setScrollBottomInset(scrollBottomInset) + ..setPadding(padding); + } +} From 06c75637682053f2f71250d6622e2d91b1213527 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Mon, 26 Apr 2021 09:07:04 -0700 Subject: [PATCH 17/19] Fix simple viewer compilation error --- lib/widgets/simple_viewer.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/widgets/simple_viewer.dart b/lib/widgets/simple_viewer.dart index 97cdcedd..ee1a7732 100644 --- a/lib/widgets/simple_viewer.dart +++ b/lib/widgets/simple_viewer.dart @@ -204,7 +204,8 @@ class _QuillSimpleViewerState extends State : null, embedBuilder, _cursorCont, - indentLevelCounts); + indentLevelCounts, + _handleCheckboxTap); result.add(editableTextBlock); } else { throw StateError('Unreachable.'); @@ -213,6 +214,12 @@ class _QuillSimpleViewerState extends State 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; From 2adebbe11bbb54126ef9322f959adb02ad2a770a Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Mon, 26 Apr 2021 09:08:47 -0700 Subject: [PATCH 18/19] Upgrade version - checkbox supports tapping --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d96c8386..62e3a30e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.2.2] +* Checkbox supports tapping. + ## [1.2.1] * Indented position not holding while editing. diff --git a/pubspec.yaml b/pubspec.yaml index 5e87f6d7..de0226ba 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.2.1 +version: 1.2.2 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 08412c167a85310293df854be1a1c4d51bf0e9b2 Mon Sep 17 00:00:00 2001 From: Gyuri Majercsik Date: Mon, 26 Apr 2021 20:09:03 +0300 Subject: [PATCH 19/19] 171: support for non-scrollable text editor (#188) Co-authored-by: Gyuri Majercsik --- lib/widgets/editor.dart | 1 - lib/widgets/raw_editor.dart | 33 ++++++++++++++++++--------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index f6dd8e40..662a018c 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -1043,7 +1043,6 @@ class RenderEditableContainerBox extends RenderBox @override void performLayout() { - assert(!constraints.hasBoundedHeight); assert(constraints.hasBoundedWidth); _resolvePadding(); assert(_resolvedPadding != null); diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index f93eabe2..4df93a4d 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -1008,25 +1008,28 @@ class RawEditorState extends EditorState _showCaretOnScreenScheduled = true; SchedulerBinding.instance!.addPostFrameCallback((_) { - _showCaretOnScreenScheduled = false; + if (widget.scrollable) { + _showCaretOnScreenScheduled = false; - final viewport = RenderAbstractViewport.of(getRenderEditor())!; - final editorOffset = getRenderEditor()! - .localToGlobal(const Offset(0, 0), ancestor: viewport); - final offsetInViewport = _scrollController!.offset + editorOffset.dy; + final viewport = RenderAbstractViewport.of(getRenderEditor()); - final offset = getRenderEditor()!.getOffsetToRevealCursor( - _scrollController!.position.viewportDimension, - _scrollController!.offset, - offsetInViewport, - ); + final editorOffset = getRenderEditor()! + .localToGlobal(const Offset(0, 0), ancestor: viewport); + final offsetInViewport = _scrollController!.offset + editorOffset.dy; - if (offset != null) { - _scrollController!.animateTo( - offset, - duration: const Duration(milliseconds: 100), - curve: Curves.fastOutSlowIn, + final offset = getRenderEditor()!.getOffsetToRevealCursor( + _scrollController!.position.viewportDimension, + _scrollController!.offset, + offsetInViewport, ); + + if (offset != null) { + _scrollController!.animateTo( + offset, + duration: const Duration(milliseconds: 100), + curve: Curves.fastOutSlowIn, + ); + } } }); }