diff --git a/.github/ISSUE_TEMPLATE/issue-template.md b/.github/ISSUE_TEMPLATE/issue-template.md index a908ed53..ff759a4b 100644 --- a/.github/ISSUE_TEMPLATE/issue-template.md +++ b/.github/ISSUE_TEMPLATE/issue-template.md @@ -11,6 +11,6 @@ My issue is about [Web] My issue is about [Mobile] My issue is about [Desktop] -I have tried running `example` directory successfully before creating an issue here AND it is NOT a stupid question. +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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3550e11a..01145d24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.1.8] +* Fix height of empty line bug. + ## [1.1.7] * Fix text selection in read-only mode. diff --git a/analysis_options.yaml b/analysis_options.yaml index 306d335b..2fc4fec6 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -3,13 +3,33 @@ include: package:pedantic/analysis_options.yaml analyzer: errors: undefined_prefixed_name: ignore - omit_local_variable_types: ignore unsafe_html: ignore linter: rules: + - always_declare_return_types - always_put_required_named_parameters_first + - annotate_overrides + - avoid_empty_else + - avoid_escaping_inner_quotes - avoid_print - avoid_redundant_argument_values + - avoid_types_on_closure_parameters + - avoid_void_async + - cascade_invocations + - directives_ordering + - omit_local_variable_types - prefer_const_constructors - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + - prefer_initializing_formals + - prefer_int_literals + - prefer_interpolation_to_compose_strings + - prefer_relative_imports + - prefer_single_quotes + - sort_constructors_first + - sort_unnamed_constructors_first - unnecessary_parenthesis + - unnecessary_string_interpolations diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 65c2f165..075f3f9b 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:app/universal_ui/universal_ui.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -16,6 +15,7 @@ import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:tuple/tuple.dart'; +import '../universal_ui/universal_ui.dart'; import 'read_only_page.dart'; class HomePage extends StatefulWidget { @@ -74,7 +74,7 @@ class _HomePageState extends State { ), body: RawKeyboardListener( focusNode: FocusNode(), - onKey: (RawKeyEvent event) { + onKey: (event) { if (event.data.isControlPressed && event.character == 'b') { if (_controller! .getSelectionStyle() @@ -107,15 +107,15 @@ class _HomePageState extends State { customStyles: DefaultStyles( h1: DefaultTextBlockStyle( const TextStyle( - fontSize: 32.0, + fontSize: 32, color: Colors.black, height: 1.15, fontWeight: FontWeight.w300, ), - const Tuple2(16.0, 0.0), - const Tuple2(0.0, 0.0), + const Tuple2(16, 0), + const Tuple2(0, 0), null), - sizeSmall: const TextStyle(fontSize: 9.0), + sizeSmall: const TextStyle(fontSize: 9), )); if (kIsWeb) { quillEditor = QuillEditor( @@ -131,15 +131,15 @@ class _HomePageState extends State { customStyles: DefaultStyles( h1: DefaultTextBlockStyle( const TextStyle( - fontSize: 32.0, + fontSize: 32, color: Colors.black, height: 1.15, fontWeight: FontWeight.w300, ), - const Tuple2(16.0, 0.0), - const Tuple2(0.0, 0.0), + const Tuple2(16, 0), + const Tuple2(0, 0), null), - sizeSmall: const TextStyle(fontSize: 9.0), + sizeSmall: const TextStyle(fontSize: 9), ), embedBuilder: defaultEmbedBuilderWeb); } @@ -151,7 +151,7 @@ class _HomePageState extends State { flex: 15, child: Container( color: Colors.white, - padding: const EdgeInsets.only(left: 16.0, right: 16.0), + padding: const EdgeInsets.only(left: 16, right: 16), child: quillEditor, ), ), @@ -178,15 +178,15 @@ class _HomePageState extends State { // You can also upload the picked image to any server (eg : AWS s3 or Firebase) and then return the uploaded image URL Future _onImagePickCallback(File file) async { // Copies the picked file from temporary cache to applications directory - Directory appDocDir = await getApplicationDocumentsDirectory(); - File copiedFile = + final appDocDir = await getApplicationDocumentsDirectory(); + final copiedFile = await file.copy('${appDocDir.path}/${basename(file.path)}'); return copiedFile.path.toString(); } Widget _buildMenuBar(BuildContext context) { - Size size = MediaQuery.of(context).size; - final itemStyle = const TextStyle( + final size = MediaQuery.of(context).size; + const itemStyle = TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, @@ -201,7 +201,7 @@ class _HomePageState extends State { endIndent: size.width * 0.1, ), ListTile( - title: Center(child: Text('Read only demo', style: itemStyle)), + title: const Center(child: Text('Read only demo', style: itemStyle)), dense: true, visualDensity: VisualDensity.compact, onTap: _readOnly, @@ -220,7 +220,7 @@ class _HomePageState extends State { Navigator.push( super.context, MaterialPageRoute( - builder: (BuildContext context) => ReadOnlyPage(), + builder: (context) => ReadOnlyPage(), ), ); } diff --git a/example/lib/pages/read_only_page.dart b/example/lib/pages/read_only_page.dart index e87e4f8f..42957b52 100644 --- a/example/lib/pages/read_only_page.dart +++ b/example/lib/pages/read_only_page.dart @@ -1,9 +1,9 @@ -import 'package:app/universal_ui/universal_ui.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_quill/widgets/controller.dart'; import 'package:flutter_quill/widgets/editor.dart'; +import '../universal_ui/universal_ui.dart'; import '../widgets/demo_scaffold.dart'; class ReadOnlyPage extends StatefulWidget { @@ -53,7 +53,7 @@ class _ReadOnlyPageState extends State { embedBuilder: defaultEmbedBuilderWeb); } return Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8), child: Container( decoration: BoxDecoration( color: Colors.white, diff --git a/example/lib/universal_ui/universal_ui.dart b/example/lib/universal_ui/universal_ui.dart index 15eeb9f8..b242af34 100644 --- a/example/lib/universal_ui/universal_ui.dart +++ b/example/lib/universal_ui/universal_ui.dart @@ -28,10 +28,10 @@ var ui = UniversalUI(); Widget defaultEmbedBuilderWeb(BuildContext context, leaf.Embed node) { switch (node.value.type) { case 'image': - String imageUrl = node.value.data; - Size size = MediaQuery.of(context).size; + final String imageUrl = node.value.data; + final size = MediaQuery.of(context).size; UniversalUI().platformViewRegistry.registerViewFactory( - imageUrl, (int viewId) => html.ImageElement()..src = imageUrl); + imageUrl, (viewId) => html.ImageElement()..src = imageUrl); return Padding( padding: EdgeInsets.only( right: ResponsiveWidget.isMediumScreen(context) diff --git a/example/lib/widgets/demo_scaffold.dart b/example/lib/widgets/demo_scaffold.dart index 52d44e4f..4098a5f6 100644 --- a/example/lib/widgets/demo_scaffold.dart +++ b/example/lib/widgets/demo_scaffold.dart @@ -11,13 +11,6 @@ typedef DemoContentBuilder = Widget Function( // Common scaffold for all examples. class DemoScaffold extends StatefulWidget { - /// Filename of the document to load into the editor. - final String documentFilename; - final DemoContentBuilder builder; - final List? actions; - final Widget? floatingActionButton; - final bool showToolbar; - const DemoScaffold({ required this.documentFilename, required this.builder, @@ -27,6 +20,13 @@ class DemoScaffold extends StatefulWidget { Key? key, }) : super(key: key); + /// Filename of the document to load into the editor. + final String documentFilename; + final DemoContentBuilder builder; + final List? actions; + final Widget? floatingActionButton; + final bool showToolbar; + @override _DemoScaffoldState createState() => _DemoScaffoldState(); } diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index 5029542e..bf233450 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -10,7 +10,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:app/main.dart'; void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { + testWidgets('Counter increments smoke test', (tester) async { // Build our app and trigger a frame. await tester.pumpWidget(MyApp()); diff --git a/lib/models/documents/attribute.dart b/lib/models/documents/attribute.dart index 8d43e805..18822e01 100644 --- a/lib/models/documents/attribute.dart +++ b/lib/models/documents/attribute.dart @@ -8,12 +8,12 @@ enum AttributeScope { } class Attribute { + Attribute(this.key, this.scope, this.value); + final String key; final AttributeScope scope; final T value; - Attribute(this.key, this.scope, this.value); - static final Map _registry = { Attribute.bold.key: Attribute.bold, Attribute.italic.key: Attribute.italic, @@ -164,8 +164,8 @@ class Attribute { if (!_registry.containsKey(key)) { throw ArgumentError.value(key, 'key "$key" not found.'); } - Attribute origin = _registry[key]!; - Attribute attribute = clone(origin, value); + final origin = _registry[key]!; + final attribute = clone(origin, value); return attribute; } @@ -177,7 +177,7 @@ class Attribute { bool operator ==(Object other) { if (identical(this, other)) return true; if (other is! Attribute) return false; - Attribute typedOther = other; + final typedOther = other; return key == typedOther.key && scope == typedOther.scope && value == typedOther.value; diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index 8a620446..0362665e 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -1,20 +1,32 @@ import 'dart:async'; -import 'package:flutter_quill/models/documents/nodes/block.dart'; -import 'package:flutter_quill/models/documents/nodes/container.dart'; -import 'package:flutter_quill/models/documents/nodes/line.dart'; -import 'package:flutter_quill/models/documents/style.dart'; -import 'package:flutter_quill/models/quill_delta.dart'; import 'package:tuple/tuple.dart'; +import '../quill_delta.dart'; import '../rules/rule.dart'; import 'attribute.dart'; import 'history.dart'; +import 'nodes/block.dart'; +import 'nodes/container.dart'; import 'nodes/embed.dart'; +import 'nodes/line.dart'; import 'nodes/node.dart'; +import 'style.dart'; /// The rich text document class Document { + Document() : _delta = Delta()..insert('\n') { + _loadDocument(_delta); + } + + Document.fromJson(List data) : _delta = _transform(Delta.fromJson(data)) { + _loadDocument(_delta); + } + + Document.fromDelta(Delta delta) : _delta = delta { + _loadDocument(delta); + } + /// The root node of the document tree final Root _root = Root(); @@ -35,18 +47,6 @@ class Document { Stream> get changes => _observer.stream; - Document() : _delta = Delta()..insert('\n') { - _loadDocument(_delta); - } - - Document.fromJson(List data) : _delta = _transform(Delta.fromJson(data)) { - _loadDocument(_delta); - } - - Document.fromDelta(Delta delta) : _delta = delta { - _loadDocument(delta); - } - Delta insert(int index, Object? data) { assert(index >= 0); assert(data is String || data is Embeddable); @@ -56,14 +56,14 @@ class Document { return Delta(); } - Delta delta = _rules.apply(RuleType.INSERT, this, index, data: data); + final delta = _rules.apply(RuleType.INSERT, this, index, data: data); compose(delta, ChangeSource.LOCAL); return delta; } Delta delete(int index, int len) { assert(index >= 0 && len > 0); - Delta delta = _rules.apply(RuleType.DELETE, this, index, len: len); + final delta = _rules.apply(RuleType.DELETE, this, index, len: len); if (delta.isNotEmpty) { compose(delta, ChangeSource.LOCAL); } @@ -74,18 +74,18 @@ class Document { assert(index >= 0); assert(data is String || data is Embeddable); - bool dataIsNotEmpty = (data is String) ? data.isNotEmpty : true; + final dataIsNotEmpty = (data is String) ? data.isNotEmpty : true; assert(dataIsNotEmpty || len > 0); - Delta delta = Delta(); + var delta = Delta(); if (dataIsNotEmpty) { delta = insert(index + len, data); } if (len > 0) { - Delta deleteDelta = delete(index, len); + final deleteDelta = delete(index, len); delta = delta.compose(deleteDelta); } @@ -95,9 +95,9 @@ class Document { Delta format(int index, int len, Attribute? attribute) { assert(index >= 0 && len >= 0 && attribute != null); - Delta delta = Delta(); + var delta = Delta(); - Delta formatDelta = _rules.apply(RuleType.FORMAT, this, index, + final formatDelta = _rules.apply(RuleType.FORMAT, this, index, len: len, attribute: attribute); if (formatDelta.isNotEmpty) { compose(formatDelta, ChangeSource.LOCAL); @@ -108,16 +108,16 @@ class Document { } Style collectStyle(int index, int len) { - ChildQuery res = queryChild(index); + final res = queryChild(index); return (res.node as Line).collectStyle(res.offset, len); } ChildQuery queryChild(int offset) { - ChildQuery res = _root.queryChild(offset, true); + final res = _root.queryChild(offset, true); if (res.node is Line) { return res; } - Block block = res.node as Block; + final block = res.node as Block; return block.queryChild(res.offset, true); } @@ -126,11 +126,11 @@ class Document { delta.trim(); assert(delta.isNotEmpty); - int offset = 0; + var offset = 0; delta = _transform(delta); - Delta originalDelta = toDelta(); - for (Operation op in delta.toList()) { - Style? style = + final originalDelta = toDelta(); + for (final op in delta.toList()) { + final style = op.attributes != null ? Style.fromJson(op.attributes) : null; if (op.isInsert) { @@ -172,10 +172,10 @@ class Document { bool get hasRedo => _history.hasRedo; static Delta _transform(Delta delta) { - Delta res = Delta(); - List ops = delta.toList(); - for (int i = 0; i < ops.length; i++) { - Operation op = ops[i]; + final res = Delta(); + final ops = delta.toList(); + for (var i = 0; i < ops.length; i++) { + final op = ops[i]; res.push(op); _handleImageInsert(i, ops, op, res); } @@ -184,14 +184,14 @@ class Document { static void _handleImageInsert( int i, List ops, Operation op, Delta res) { - bool nextOpIsImage = + final nextOpIsImage = i + 1 < ops.length && ops[i + 1].isInsert && ops[i + 1].data is! String; if (nextOpIsImage && !(op.data as String).endsWith('\n')) { res.push(Operation.insert('\n')); } // Currently embed is equivalent to image and hence `is! String` - bool opInsertImage = op.isInsert && op.data is! String; - bool nextOpIsLineBreak = i + 1 < ops.length && + final opInsertImage = op.isInsert && op.data is! String; + final nextOpIsLineBreak = i + 1 < ops.length && ops[i + 1].isInsert && ops[i + 1].data is String && (ops[i + 1].data as String).startsWith('\n'); @@ -221,7 +221,7 @@ class Document { void _loadDocument(Delta doc) { assert((doc.last.data as String).endsWith('\n')); - int offset = 0; + var offset = 0; for (final op in doc.toList()) { if (!op.isInsert) { throw ArgumentError.value(doc, @@ -247,12 +247,12 @@ class Document { return false; } - final Node node = root.children.first; + final node = root.children.first; if (!node.isLast) { return false; } - Delta delta = node.toDelta(); + final delta = node.toDelta(); return delta.length == 1 && delta.first.data == '\n' && delta.first.key == 'insert'; diff --git a/lib/models/documents/history.dart b/lib/models/documents/history.dart index 6d89b389..d406505e 100644 --- a/lib/models/documents/history.dart +++ b/lib/models/documents/history.dart @@ -1,9 +1,17 @@ -import 'package:flutter_quill/models/quill_delta.dart'; import 'package:tuple/tuple.dart'; +import '../quill_delta.dart'; import 'document.dart'; class History { + History({ + this.ignoreChange = false, + this.interval = 400, + this.maxStack = 100, + this.userOnly = false, + this.lastRecorded = 0, + }); + final HistoryStack stack = HistoryStack.empty(); bool get hasUndo => stack.undo.isNotEmpty; @@ -24,13 +32,6 @@ class History { ///record delay final int interval; - History( - {this.ignoreChange = false, - this.interval = 400, - this.maxStack = 100, - this.userOnly = false, - this.lastRecorded = 0}); - void handleDocChange(Tuple3 change) { if (ignoreChange) return; if (!userOnly || change.item3 == ChangeSource.LOCAL) { @@ -47,7 +48,7 @@ class History { void record(Delta change, Delta before) { if (change.isEmpty) return; stack.redo.clear(); - Delta undoDelta = change.invert(before); + var undoDelta = change.invert(before); final timeStamp = DateTime.now().millisecondsSinceEpoch; if (lastRecorded + interval > timeStamp && stack.undo.isNotEmpty) { @@ -74,7 +75,7 @@ class History { } void transformStack(List stack, Delta delta) { - for (int i = stack.length - 1; i >= 0; i -= 1) { + for (var i = stack.length - 1; i >= 0; i -= 1) { final oldDelta = stack[i]; stack[i] = delta.transform(oldDelta, true); delta = oldDelta.transform(delta, false); @@ -88,10 +89,10 @@ class History { if (source.isEmpty) { return const Tuple2(false, 0); } - Delta delta = source.removeLast(); + final delta = source.removeLast(); // look for insert or delete int? len = 0; - List ops = delta.toList(); + final ops = delta.toList(); for (var i = 0; i < ops.length; i++) { if (ops[i].key == Operation.insertKey) { len = ops[i].length; @@ -99,8 +100,8 @@ class History { len = ops[i].length! * -1; } } - Delta base = Delta.from(doc.toDelta()); - Delta inverseDelta = delta.invert(base); + final base = Delta.from(doc.toDelta()); + final inverseDelta = delta.invert(base); dest.add(inverseDelta); lastRecorded = 0; ignoreChange = true; @@ -119,13 +120,13 @@ class History { } class HistoryStack { - final List undo; - final List redo; - HistoryStack.empty() : undo = [], redo = []; + final List undo; + final List redo; + void clear() { undo.clear(); redo.clear(); diff --git a/lib/models/documents/nodes/block.dart b/lib/models/documents/nodes/block.dart index 4d569cc7..095f1183 100644 --- a/lib/models/documents/nodes/block.dart +++ b/lib/models/documents/nodes/block.dart @@ -1,10 +1,23 @@ -import 'package:flutter_quill/models/quill_delta.dart'; - +import '../../quill_delta.dart'; import 'container.dart'; import 'line.dart'; import 'node.dart'; +/// Represents a group of adjacent [Line]s with the same block style. +/// +/// Block elements are: +/// - Blockquote +/// - Header +/// - Indent +/// - List +/// - Text Alignment +/// - Text Direction +/// - Code Block class Block extends Container { + /// Creates new unmounted [Block]. + @override + Node newInstance() => Block(); + @override Line get defaultChild => Line(); @@ -18,7 +31,7 @@ class Block extends Container { @override void adjust() { if (isEmpty) { - Node? sibling = previous; + final sibling = previous; unlink(); if (sibling != null) { sibling.adjust(); @@ -26,17 +39,18 @@ class Block extends Container { return; } - Block block = this; - Node? prev = block.previous; + var block = this; + final prev = block.previous; // merging it with previous block if style is the same if (!block.isFirst && block.previous is Block && prev!.style == block.style) { - block.moveChildToNewParent(prev as Container?); - block.unlink(); + block + ..moveChildToNewParent(prev as Container?) + ..unlink(); block = prev as Block; } - Node? next = block.next; + final next = block.next; // merging it with next block if style is the same if (!block.isLast && block.next is Block && next!.style == block.style) { (next as Block).moveChildToNewParent(block); @@ -48,16 +62,11 @@ class Block extends Container { String toString() { final block = style.attributes.toString(); final buffer = StringBuffer('§ {$block}\n'); - for (var child in children) { + for (final child in children) { final tree = child.isLast ? '└' : '├'; buffer.write(' $tree $child'); if (!child.isLast) buffer.writeln(); } return buffer.toString(); } - - @override - Node newInstance() { - return Block(); - } } diff --git a/lib/models/documents/nodes/container.dart b/lib/models/documents/nodes/container.dart index c7c10390..dbdd12d1 100644 --- a/lib/models/documents/nodes/container.dart +++ b/lib/models/documents/nodes/container.dart @@ -1,70 +1,99 @@ import 'dart:collection'; import '../style.dart'; +import 'leaf.dart'; +import 'line.dart'; import 'node.dart'; -/* Container of multiple nodes */ +/// Container can accommodate other nodes. +/// +/// Delegates insert, retain and delete operations to children nodes. For each +/// operation container looks for a child at specified index position and +/// forwards operation to that child. +/// +/// Most of the operation handling logic is implemented by [Line] and [Text]. abstract class Container extends Node { final LinkedList _children = LinkedList(); + /// List of children. LinkedList get children => _children; + /// Returns total number of child nodes in this container. + /// + /// To get text length of this container see [length]. int get childCount => _children.length; + /// Returns the first child [Node]. Node get first => _children.first; + /// Returns the last child [Node]. Node get last => _children.last; + /// Returns `true` if this container has no child nodes. bool get isEmpty => _children.isEmpty; + /// Returns `true` if this container has at least 1 child. bool get isNotEmpty => _children.isNotEmpty; - /// abstract methods begin - + /// Returns an instance of default child for this container node. + /// + /// Always returns fresh instance. T get defaultChild; - /// abstract methods end - + /// Adds [node] to the end of this container children list. void add(T node) { assert(node?.parent == null); node?.parent = this; _children.add(node as Node); } + /// Adds [node] to the beginning of this container children list. void addFirst(T node) { assert(node?.parent == null); node?.parent = this; _children.addFirst(node as Node); } + /// Removes [node] from this container. void remove(T node) { assert(node?.parent == this); node?.parent = null; _children.remove(node as Node); } + /// Moves children of this node to [newParent]. void moveChildToNewParent(Container? newParent) { if (isEmpty) { return; } - T? last = newParent!.isEmpty ? null : newParent.last as T?; + final last = newParent!.isEmpty ? null : newParent.last as T?; while (isNotEmpty) { - T child = first as T; + final child = first as T; child?.unlink(); newParent.add(child); } + /// In case [newParent] already had children we need to make sure + /// combined list is optimized. if (last != null) last.adjust(); } + /// Queries the child [Node] at specified character [offset] in this container. + /// + /// The result may contain the found node or `null` if no node is found + /// at specified offset. + /// + /// [ChildQuery.offset] is set to relative offset within returned child node + /// which points at the same character position in the document as the + /// original [offset]. ChildQuery queryChild(int offset, bool inclusive) { if (offset < 0 || offset > length) { return ChildQuery(null, 0); } - for (Node node in children) { - int len = node.length; + for (final node in children) { + final len = node.length; if (offset < len || (inclusive && offset == len && (node.isLast))) { return ChildQuery(node, offset); } @@ -76,6 +105,9 @@ abstract class Container extends Node { @override String toPlainText() => children.map((child) => child.toPlainText()).join(); + /// Content length of this node's children. + /// + /// To get number of children in this node use [childCount]. @override int get length => _children.fold(0, (cur, node) => cur + node.length); @@ -84,14 +116,14 @@ abstract class Container extends Node { assert(index == 0 || (index > 0 && index < length)); if (isNotEmpty) { - ChildQuery child = queryChild(index, false); + final child = queryChild(index, false); child.node!.insert(child.offset, data, style); return; } // empty assert(index == 0); - T node = defaultChild; + final node = defaultChild; add(node); node?.insert(index, data, style); } @@ -99,14 +131,14 @@ abstract class Container extends Node { @override void retain(int index, int? length, Style? attributes) { assert(isNotEmpty); - ChildQuery child = queryChild(index, false); + final child = queryChild(index, false); child.node!.retain(child.offset, length, attributes); } @override void delete(int index, int? length) { assert(isNotEmpty); - ChildQuery child = queryChild(index, false); + final child = queryChild(index, false); child.node!.delete(child.offset, length); } @@ -114,11 +146,15 @@ abstract class Container extends Node { String toString() => _children.join('\n'); } -/// Query of a child in a Container +/// Result of a child query in a [Container]. class ChildQuery { - final Node? node; // null if not found + ChildQuery(this.node, this.offset); - final int offset; + /// The child node if found, otherwise `null`. + final Node? node; - ChildQuery(this.node, this.offset); + /// Starting offset within the child [node] which points at the same + /// character in the document as the original offset passed to + /// [Container.queryChild] method. + final int offset; } diff --git a/lib/models/documents/nodes/embed.dart b/lib/models/documents/nodes/embed.dart index 3268e257..d6fe628a 100644 --- a/lib/models/documents/nodes/embed.dart +++ b/lib/models/documents/nodes/embed.dart @@ -1,26 +1,40 @@ +/// An object which can be embedded into a Quill document. +/// +/// See also: +/// +/// * [BlockEmbed] which represents a block embed. class Embeddable { + Embeddable(this.type, this.data); + + /// The type of this object. final String type; - final dynamic data; - Embeddable(this.type, this.data); + /// The data payload of this object. + final dynamic data; Map toJson() { - Map m = {type: data}; + final m = {type: data}; return m; } static Embeddable fromJson(Map json) { - Map m = Map.from(json); + final m = Map.from(json); assert(m.length == 1, 'Embeddable map has one key'); return BlockEmbed(m.keys.first, m.values.first); } } +/// An object which occupies an entire line in a document and cannot co-exist +/// inline with regular text. +/// +/// There are two built-in embed types supported by Quill documents, however +/// the document model itself does not make any assumptions about the types +/// of embedded objects and allows users to define their own types. class BlockEmbed extends Embeddable { BlockEmbed(String type, String data) : super(type, data); - static final BlockEmbed horizontalRule = BlockEmbed('divider', 'hr'); + static BlockEmbed horizontalRule = BlockEmbed('divider', 'hr'); static BlockEmbed image(String imageUrl) => BlockEmbed('image', imageUrl); } diff --git a/lib/models/documents/nodes/leaf.dart b/lib/models/documents/nodes/leaf.dart index 6cb9af24..bd9292f5 100644 --- a/lib/models/documents/nodes/leaf.dart +++ b/lib/models/documents/nodes/leaf.dart @@ -1,29 +1,30 @@ import 'dart:math' as math; -import 'package:flutter_quill/models/quill_delta.dart'; - +import '../../quill_delta.dart'; import '../style.dart'; import 'embed.dart'; import 'line.dart'; import 'node.dart'; -/* A leaf node in document tree */ +/// A leaf in Quill document tree. abstract class Leaf extends Node { - Object _value; - - Object get value => _value; - - Leaf.val(Object val) : _value = val; - + /// Creates a new [Leaf] with specified [data]. factory Leaf(Object data) { if (data is Embeddable) { return Embed(data); } - String text = data as String; + final text = data as String; assert(text.isNotEmpty); return Text(text); } + Leaf.val(Object val) : _value = val; + + /// Contents of this node, either a String if this is a [Text] or an + /// [Embed] if this is an [BlockEmbed]. + Object get value => _value; + Object _value; + @override void applyStyle(Style value) { assert(value.isInline || value.isIgnored || value.isEmpty, @@ -45,14 +46,15 @@ abstract class Leaf extends Node { @override Delta toDelta() { - var data = _value is Embeddable ? (_value as Embeddable).toJson() : _value; + final data = + _value is Embeddable ? (_value as Embeddable).toJson() : _value; return Delta()..insert(data, style.toJson()); } @override void insert(int index, Object data, Style? style) { assert(index >= 0 && index <= length); - Leaf node = Leaf(data); + final node = Leaf(data); if (index < length) { splitAt(index)!.insertBefore(node); } else { @@ -67,9 +69,9 @@ abstract class Leaf extends Node { return; } - int local = math.min(length - index, len!); - int remain = len - local; - Leaf node = _isolate(index, local); + final local = math.min(length - index, len!); + final remain = len - local; + final node = _isolate(index, local); if (remain > 0) { assert(node.next != null); @@ -82,13 +84,13 @@ abstract class Leaf extends Node { void delete(int index, int? len) { assert(index < length); - int local = math.min(length - index, len!); - Leaf target = _isolate(index, local); - Leaf? prev = target.previous as Leaf?; - Leaf? next = target.next as Leaf?; + final local = math.min(length - index, len!); + final target = _isolate(index, local); + final prev = target.previous as Leaf?; + final next = target.next as Leaf?; target.unlink(); - int remain = len - local; + final remain = len - local; if (remain > 0) { assert(next != null); next!.delete(0, remain); @@ -99,36 +101,47 @@ abstract class Leaf extends Node { } } + /// Adjust this text node by merging it with adjacent nodes if they share + /// the same style. @override void adjust() { if (this is Embed) { + // Embed nodes cannot be merged with text nor other embeds (in fact, + // there could be no two adjacent embeds on the same line since an + // embed occupies an entire line). return; } - Text node = this as Text; - // merging it with previous node if style is the same - Node? prev = node.previous; + // This is a text node and it can only be merged with other text nodes. + var node = this as Text; + + // Merging it with previous node if style is the same. + final prev = node.previous; if (!node.isFirst && prev is Text && prev.style == node.style) { prev._value = prev.value + node.value; node.unlink(); node = prev; } - // merging it with next node if style is the same - Node? next = node.next; + // Merging it with next node if style is the same. + final next = node.next; if (!node.isLast && next is Text && next.style == node.style) { node._value = node.value + next.value; next.unlink(); } } - Leaf? cutAt(int index) { - assert(index >= 0 && index <= length); - Leaf? cut = splitAt(index); - cut?.unlink(); - return cut; - } - + /// Splits this leaf node at [index] and returns new node. + /// + /// If this is the last node in its list and [index] equals this node's + /// length then this method returns `null` as there is nothing left to split. + /// If there is another leaf node after this one and [index] equals this + /// node's length then the next leaf node is returned. + /// + /// If [index] equals to `0` then this node itself is returned unchanged. + /// + /// In case a new node is actually split from this one, it inherits this + /// node's style. Leaf? splitAt(int index) { assert(index >= 0 && index <= length); if (index == 0) { @@ -139,64 +152,101 @@ abstract class Leaf extends Node { } assert(this is Text); - String text = _value as String; + final text = _value as String; _value = text.substring(0, index); - Leaf split = Leaf(text.substring(index)); - split.applyStyle(style); + final split = Leaf(text.substring(index))..applyStyle(style); insertAfter(split); return split; } + /// Cuts a leaf from [index] to the end of this node and returns new node + /// in detached state (e.g. [mounted] returns `false`). + /// + /// Splitting logic is identical to one described in [splitAt], meaning this + /// method may return `null`. + Leaf? cutAt(int index) { + assert(index >= 0 && index <= length); + final cut = splitAt(index); + cut?.unlink(); + return cut; + } + + /// Formats this node and optimizes it with adjacent leaf nodes if needed. void format(Style? style) { if (style != null && style.isNotEmpty) { applyStyle(style); } - adjust(); } + /// Isolates a new leaf starting at [index] with specified [length]. + /// + /// Splitting logic is identical to one described in [splitAt], with one + /// exception that it is required for [index] to always be less than this + /// node's length. As a result this method always returns a [LeafNode] + /// instance. Returned node may still be the same as this node + /// if provided [index] is `0`. Leaf _isolate(int index, int length) { assert( index >= 0 && index < this.length && (index + length <= this.length)); - Leaf target = splitAt(index)!; - target.splitAt(length); + final target = splitAt(index)!..splitAt(length); return target; } } +/// A span of formatted text within a line in a Quill document. +/// +/// Text is a leaf node of a document tree. +/// +/// Parent of a text node is always a [Line], and as a consequence text +/// node's [value] cannot contain any line-break characters. +/// +/// See also: +/// +/// * [Embed], a leaf node representing an embeddable object. +/// * [Line], a node representing a line of text. class Text extends Leaf { Text([String text = '']) : assert(!text.contains('\n')), super.val(text); @override - String get value => _value as String; + Node newInstance() => Text(); @override - String toPlainText() { - return value; - } + String get value => _value as String; @override - Node newInstance() { - return Text(); - } + String toPlainText() => value; } -/// An embedded node such as image or video +/// An embed node inside of a line in a Quill document. +/// +/// Embed node is a leaf node similar to [Text]. It represents an arbitrary +/// piece of non-textual content embedded into a document, such as, image, +/// horizontal rule, video, or any other object with defined structure, +/// like a tweet, for instance. +/// +/// Embed node's length is always `1` character and it is represented with +/// unicode object replacement character in the document text. +/// +/// Any inline style can be applied to an embed, however this does not +/// necessarily mean the embed will look according to that style. For instance, +/// applying "bold" style to an image gives no effect, while adding a "link" to +/// an image actually makes the image react to user's action. class Embed extends Leaf { Embed(Embeddable data) : super.val(data); + static const kObjectReplacementCharacter = '\uFFFC'; + @override - Embeddable get value => super.value as Embeddable; + Node newInstance() => throw UnimplementedError(); @override - String toPlainText() { - return '\uFFFC'; - } + Embeddable get value => super.value as Embeddable; + /// // Embed nodes are represented as unicode object replacement character in + // plain text. @override - Node newInstance() { - throw UnimplementedError(); - } + String toPlainText() => kObjectReplacementCharacter; } diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart index 574549ea..ec933b52 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -1,15 +1,20 @@ import 'dart:math' as math; -import 'package:flutter_quill/models/documents/attribute.dart'; -import 'package:flutter_quill/models/documents/nodes/node.dart'; -import 'package:flutter_quill/models/quill_delta.dart'; - +import '../../quill_delta.dart'; +import '../attribute.dart'; import '../style.dart'; import 'block.dart'; import 'container.dart'; import 'embed.dart'; import 'leaf.dart'; - +import 'node.dart'; + +/// A line of rich text in a Quill document. +/// +/// Line serves as a container for [Leaf]s, like [Text] and [Embed]. +/// +/// When a line contains an embed, it fully occupies the line, no other embeds +/// or text nodes are allowed. class Line extends Container { @override Leaf get defaultChild => Text(); @@ -17,6 +22,7 @@ class Line extends Container { @override int get length => super.length + 1; + /// Returns `true` if this line contains an embedded object. bool get hasEmbed { if (childCount != 1) { return false; @@ -25,6 +31,7 @@ class Line extends Container { return children.single is Embed; } + /// Returns next [Line] or `null` if this is the last line in the document. Line? get nextLine { if (!isLast) { return next is Block ? (next as Block).first as Line? : next as Line?; @@ -41,6 +48,9 @@ class Line extends Container { : parent!.next as Line?; } + @override + Node newInstance() => Line(); + @override Delta toDelta() { final delta = children @@ -48,7 +58,7 @@ class Line extends Container { .fold(Delta(), (dynamic a, b) => a.concat(b)); var attributes = style; if (parent is Block) { - Block block = parent as Block; + final block = parent as Block; attributes = attributes.mergeAll(block.style); } delta.insert('\n', attributes.toJson()); @@ -56,7 +66,7 @@ class Line extends Container { } @override - String toPlainText() => super.toPlainText() + '\n'; + String toPlainText() => '${super.toPlainText()}\n'; @override String toString() { @@ -68,35 +78,43 @@ class Line extends Container { @override void insert(int index, Object data, Style? style) { if (data is Embeddable) { - _insert(index, data, style); + // We do not check whether this line already has any children here as + // inserting an embed into a line with other text is acceptable from the + // Delta format perspective. + // We rely on heuristic rules to ensure that embeds occupy an entire line. + _insertSafe(index, data, style); return; } - String text = data as String; - int lineBreak = text.indexOf('\n'); + final text = data as String; + final lineBreak = text.indexOf('\n'); if (lineBreak < 0) { - _insert(index, text, style); + _insertSafe(index, text, style); + // No need to update line or block format since those attributes can only + // be attached to `\n` character and we already know it's not present. return; } - String prefix = text.substring(0, lineBreak); - _insert(index, prefix, style); + final prefix = text.substring(0, lineBreak); + _insertSafe(index, prefix, style); if (prefix.isNotEmpty) { index += prefix.length; } - Line nextLine = _getNextLine(index); + // Next line inherits our format. + final nextLine = _getNextLine(index); + // Reset our format and unwrap from a block if needed. clearStyle(); - if (parent is Block) { _unwrap(); } + // Now we can apply new format and re-layout. _format(style); - // Continue with the remaining - String remain = text.substring(lineBreak + 1); + // Continue with remaining part. + final remain = text.substring(lineBreak + 1); nextLine.insert(0, remain, style); } @@ -105,20 +123,24 @@ class Line extends Container { if (style == null) { return; } - int thisLen = length; + final thisLength = length; - int local = math.min(thisLen - index, len!); + final local = math.min(thisLength - index, len!); + // If index is at newline character then this is a line/block style update. + final isLineFormat = (index + local == thisLength) && local == 1; - if (index + local == thisLen && local == 1) { - assert(style.values.every((attr) => attr.scope == AttributeScope.BLOCK)); + if (isLineFormat) { + assert(style.values.every((attr) => attr.scope == AttributeScope.BLOCK), + 'It is not allowed to apply inline attributes to line itself.'); _format(style); } else { + // Otherwise forward to children as it's an inline format update. assert(style.values.every((attr) => attr.scope == AttributeScope.INLINE)); - assert(index + local != thisLen); + assert(index + local != thisLength); super.retain(index, local, style); } - int remain = len - local; + final remain = len - local; if (remain > 0) { assert(nextLine != null); nextLine!.retain(0, remain, style); @@ -127,66 +149,80 @@ class Line extends Container { @override void delete(int index, int? len) { - int local = math.min(length - index, len!); - bool deleted = index + local == length; - if (deleted) { + final local = math.min(length - index, len!); + final isLFDeleted = index + local == length; // Line feed + if (isLFDeleted) { + // Our newline character deleted with all style information. clearStyle(); if (local > 1) { + // Exclude newline character from delete range for children. super.delete(index, local - 1); } } else { super.delete(index, local); } - int remain = len - local; - if (remain > 0) { + final remaining = len - local; + if (remaining > 0) { assert(nextLine != null); - nextLine!.delete(0, remain); + nextLine!.delete(0, remaining); } - if (deleted && isNotEmpty) { + if (isLFDeleted && isNotEmpty) { + // Since we lost our line-break and still have child text nodes those must + // migrate to the next line. + + // nextLine might have been unmounted since last assert so we need to + // check again we still have a line after us. assert(nextLine != null); + + // Move remaining children in this line to the next line so that all + // attributes of nextLine are preserved. nextLine!.moveChildToNewParent(this); moveChildToNewParent(nextLine); } - if (deleted) { - Node p = parent!; + if (isLFDeleted) { + // Now we can remove this line. + final block = parent!; // remember reference before un-linking. unlink(); - p.adjust(); + block.adjust(); } } + /// Formats this line. void _format(Style? newStyle) { if (newStyle == null || newStyle.isEmpty) { return; } applyStyle(newStyle); - Attribute? blockStyle = newStyle.getBlockExceptHeader(); + final blockStyle = newStyle.getBlockExceptHeader(); if (blockStyle == null) { return; - } + } // No block-level changes if (parent is Block) { - Attribute? parentStyle = (parent as Block).style.getBlockExceptHeader(); + final parentStyle = (parent as Block).style.getBlockExceptHeader(); if (blockStyle.value == null) { _unwrap(); } else if (blockStyle != parentStyle) { _unwrap(); - Block block = Block(); - block.applyAttribute(blockStyle); + final block = Block()..applyAttribute(blockStyle); _wrap(block); block.adjust(); - } + } // else the same style, no-op. } else if (blockStyle.value != null) { - Block block = Block(); - block.applyAttribute(blockStyle); + // Only wrap with a new block if this is not an unset + final block = Block()..applyAttribute(blockStyle); _wrap(block); block.adjust(); } } + /// Wraps this line with new parent [block]. + /// + /// This line can not be in a [Block] when this method is called. void _wrap(Block block) { assert(parent != null && parent is! Block); insertAfter(block); @@ -194,11 +230,14 @@ class Line extends Container { block.add(this); } + /// Unwraps this line from it's parent [Block]. + /// + /// This method asserts if current [parent] of this line is not a [Block]. void _unwrap() { if (parent is! Block) { throw ArgumentError('Invalid parent'); } - Block block = parent as Block; + final block = parent as Block; assert(block.children.contains(this)); @@ -209,10 +248,10 @@ class Line extends Container { unlink(); block.insertAfter(this); } else { - Block before = block.clone() as Block; + final before = block.clone() as Block; block.insertBefore(before); - Line child = block.first as Line; + var child = block.first as Line; while (child != this) { child.unlink(); before.add(child); @@ -227,26 +266,25 @@ class Line extends Container { Line _getNextLine(int index) { assert(index == 0 || (index > 0 && index < length)); - Line line = clone() as Line; + final line = clone() as Line; insertAfter(line); if (index == length - 1) { return line; } - ChildQuery query = queryChild(index, false); + final query = queryChild(index, false); while (!query.node!.isLast) { - Leaf next = last as Leaf; - next.unlink(); + final next = (last as Leaf)..unlink(); line.addFirst(next); } - Leaf child = query.node as Leaf; - Leaf? cut = child.splitAt(query.offset); + final child = query.node as Leaf; + final cut = child.splitAt(query.offset); cut?.unlink(); line.addFirst(cut); return line; } - void _insert(int index, Object data, Style? style) { + void _insertSafe(int index, Object data, Style? style) { assert(index == 0 || (index > 0 && index < length)); if (data is String) { @@ -256,47 +294,51 @@ class Line extends Container { } } - if (isNotEmpty) { - ChildQuery result = queryChild(index, true); + if (isEmpty) { + final child = Leaf(data); + add(child); + child.format(style); + } else { + final result = queryChild(index, true); result.node!.insert(result.offset, data, style); - return; } - - Leaf child = Leaf(data); - add(child); - child.format(style); - } - - @override - Node newInstance() { - return Line(); } + /// Returns style for specified text range. + /// + /// Only attributes applied to all characters within this range are + /// included in the result. Inline and line level attributes are + /// handled separately, e.g.: + /// + /// - line attribute X is included in the result only if it exists for + /// every line within this range (partially included lines are counted). + /// - inline attribute X is included in the result only if it exists + /// for every character within this range (line-break characters excluded). Style collectStyle(int offset, int len) { - int local = math.min(length - offset, len); - Style res = Style(); - var excluded = {}; + final local = math.min(length - offset, len); + var result = Style(); + final excluded = {}; void _handle(Style style) { - if (res.isEmpty) { + if (result.isEmpty) { excluded.addAll(style.values); } else { - for (Attribute attr in res.values) { + for (final attr in result.values) { if (!style.containsKey(attr.key)) { excluded.add(attr); } } } - Style remain = style.removeAll(excluded); - res = res.removeAll(excluded); - res = res.mergeAll(remain); + final remaining = style.removeAll(excluded); + result = result.removeAll(excluded); + result = result.mergeAll(remaining); } - ChildQuery data = queryChild(offset, true); - Leaf? node = data.node as Leaf?; + final data = queryChild(offset, true); + var node = data.node as Leaf?; if (node != null) { - res = res.mergeAll(node.style); - int pos = node.length - data.offset; + result = result.mergeAll(node.style); + var pos = node.length - data.offset; while (!node!.isLast && pos < local) { node = node.next as Leaf?; _handle(node!.style); @@ -304,17 +346,18 @@ class Line extends Container { } } - res = res.mergeAll(style); + result = result.mergeAll(style); if (parent is Block) { - Block block = parent as Block; - res = res.mergeAll(block.style); + final block = parent as Block; + result = result.mergeAll(block.style); } - int remain = len - local; - if (remain > 0) { - _handle(nextLine!.collectStyle(0, remain)); + final remaining = len - local; + if (remaining > 0) { + final rest = nextLine!.collectStyle(0, remaining); + _handle(rest); } - return res; + return result; } } diff --git a/lib/models/documents/nodes/node.dart b/lib/models/documents/nodes/node.dart index abc093c3..6bb0fb97 100644 --- a/lib/models/documents/nodes/node.dart +++ b/lib/models/documents/nodes/node.dart @@ -1,51 +1,49 @@ import 'dart:collection'; -import 'package:flutter_quill/models/documents/style.dart'; -import 'package:flutter_quill/models/quill_delta.dart'; - +import '../../quill_delta.dart'; import '../attribute.dart'; +import '../style.dart'; import 'container.dart'; import 'line.dart'; -/* node in a document tree */ +/// An abstract node in a document tree. +/// +/// Represents a segment of a Quill document with specified [offset] +/// and [length]. +/// +/// 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. abstract class Node extends LinkedListEntry { + /// Current parent of this node. May be null if this node is not mounted. Container? parent; - Style _style = Style(); Style get style => _style; + Style _style = Style(); - void applyAttribute(Attribute attribute) { - _style = _style.merge(attribute); - } - - void applyStyle(Style value) { - _style = _style.mergeAll(value); - } - - void clearStyle() { - _style = Style(); - } - + /// Returns `true` if this node is the first node in the [parent] list. bool get isFirst => list!.first == this; + /// Returns `true` if this node is the last node in the [parent] list. bool get isLast => list!.last == this; + /// Length of this node in characters. int get length; - Node clone() { - Node node = newInstance(); - node.applyStyle(style); - return node; - } + Node clone() => newInstance()..applyStyle(style); - int getOffset() { - int offset = 0; + /// Offset in characters of this node relative to [parent] node. + /// + /// To get offset of this node in the document see [documentOffset]. + int get offset { + var offset = 0; if (list == null || isFirst) { return offset; } - Node cur = this; + var cur = this; do { cur = cur.previous!; offset += cur.length; @@ -53,16 +51,31 @@ abstract class Node extends LinkedListEntry { return offset; } - int getDocumentOffset() { - final parentOffset = (parent is! Root) ? parent!.getDocumentOffset() : 0; - return parentOffset + getOffset(); + /// Offset in characters of this node in the document. + int get documentOffset { + final parentOffset = (parent is! Root) ? parent!.documentOffset : 0; + return parentOffset + offset; } + /// Returns `true` if this node contains character at specified [offset] in + /// the document. bool containsOffset(int offset) { - final o = getDocumentOffset(); + final o = documentOffset; return o <= offset && offset < o + length; } + void applyAttribute(Attribute attribute) { + _style = _style.merge(attribute); + } + + void applyStyle(Style value) { + _style = _style.mergeAll(value); + } + + void clearStyle() { + _style = Style(); + } + @override void insertBefore(Node entry) { assert(entry.parent == null && parent != null); @@ -84,9 +97,7 @@ abstract class Node extends LinkedListEntry { super.unlink(); } - void adjust() { - // do nothing - } + void adjust() {/* no-op */} /// abstract methods begin @@ -103,11 +114,13 @@ abstract class Node extends LinkedListEntry { void delete(int index, int? len); /// abstract methods end - } -/* Root node of document tree */ +/// Root node of document tree. class Root extends Container> { + @override + Node newInstance() => Root(); + @override Container get defaultChild => Line(); @@ -115,9 +128,4 @@ class Root extends Container> { Delta toDelta() => children .map((child) => child.toDelta()) .fold(Delta(), (a, b) => a.concat(b)); - - @override - Node newInstance() { - return Root(); - } } diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index 7b9b050b..c805280d 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -1,22 +1,23 @@ import 'package:collection/collection.dart'; -import 'package:flutter_quill/models/documents/attribute.dart'; import 'package:quiver/core.dart'; +import 'attribute.dart'; + /* Collection of style attributes */ class Style { - final Map _attributes; + Style() : _attributes = {}; Style.attr(this._attributes); - Style() : _attributes = {}; + final Map _attributes; static Style fromJson(Map? attributes) { if (attributes == null) { return Style(); } - Map result = attributes.map((String key, dynamic value) { - Attribute attr = Attribute.fromKeyValue(key, value); + final result = attributes.map((key, dynamic value) { + final attr = Attribute.fromKeyValue(key, value); return MapEntry(key, attr); }); return Style.attr(result); @@ -24,7 +25,7 @@ class Style { Map? toJson() => _attributes.isEmpty ? null - : _attributes.map((String _, Attribute attribute) => + : _attributes.map((_, attribute) => MapEntry(attribute.key, attribute.value)); Iterable get keys => _attributes.keys; @@ -47,7 +48,7 @@ class Style { bool containsKey(String key) => _attributes.containsKey(key); Attribute? getBlockExceptHeader() { - for (Attribute val in values) { + for (final val in values) { if (val.isBlockExceptHeader) { return val; } @@ -56,7 +57,7 @@ class Style { } Style merge(Attribute attribute) { - Map merged = Map.from(_attributes); + final merged = Map.from(_attributes); if (attribute.value == null) { merged.remove(attribute.key); } else { @@ -66,21 +67,21 @@ class Style { } Style mergeAll(Style other) { - Style result = Style.attr(_attributes); - for (Attribute attribute in other.values) { + var result = Style.attr(_attributes); + for (final attribute in other.values) { result = result.merge(attribute); } return result; } Style removeAll(Set attributes) { - Map merged = Map.from(_attributes); + final merged = Map.from(_attributes); attributes.map((item) => item.key).forEach(merged.remove); return Style.attr(merged); } Style put(Attribute attribute) { - Map m = Map.from(attributes); + final m = Map.from(attributes); m[attribute.key] = attribute; return Style.attr(m); } @@ -93,8 +94,8 @@ class Style { if (other is! Style) { return false; } - Style typedOther = other; - final eq = const MapEquality(); + final typedOther = other; + const eq = MapEquality(); return eq.equals(_attributes, typedOther._attributes); } diff --git a/lib/models/quill_delta.dart b/lib/models/quill_delta.dart index ddccb710..895b27fe 100644 --- a/lib/models/quill_delta.dart +++ b/lib/models/quill_delta.dart @@ -22,6 +22,29 @@ Object? _passThroughDataDecoder(Object? data) => data; /// Operation performed on a rich-text document. class Operation { + Operation._(this.key, this.length, this.data, Map? attributes) + : assert(_validKeys.contains(key), 'Invalid operation key "$key".'), + assert(() { + if (key != Operation.insertKey) return true; + return data is String ? data.length == length : length == 1; + }(), 'Length of insert operation must be equal to the data length.'), + _attributes = + attributes != null ? Map.from(attributes) : null; + + /// Creates operation which deletes [length] of characters. + factory Operation.delete(int length) => + Operation._(Operation.deleteKey, length, '', null); + + /// Creates operation which inserts [text] with optional [attributes]. + factory Operation.insert(dynamic data, [Map? attributes]) => + Operation._(Operation.insertKey, data is String ? data.length : 1, data, + attributes); + + /// Creates operation which retains [length] of characters and optionally + /// applies attributes. + factory Operation.retain(int? length, [Map? attributes]) => + Operation._(Operation.retainKey, length, '', attributes); + /// Key of insert operations. static const String insertKey = 'insert'; @@ -50,15 +73,6 @@ class Operation { _attributes == null ? null : Map.from(_attributes!); final Map? _attributes; - Operation._(this.key, this.length, this.data, Map? attributes) - : assert(_validKeys.contains(key), 'Invalid operation key "$key".'), - assert(() { - if (key != Operation.insertKey) return true; - return data is String ? data.length == length : length == 1; - }(), 'Length of insert operation must be equal to the data length.'), - _attributes = - attributes != null ? Map.from(attributes) : null; - /// Creates new [Operation] from JSON payload. /// /// If `dataDecoder` parameter is not null then it is used to additionally @@ -89,20 +103,6 @@ class Operation { return json; } - /// Creates operation which deletes [length] of characters. - factory Operation.delete(int length) => - Operation._(Operation.deleteKey, length, '', null); - - /// Creates operation which inserts [text] with optional [attributes]. - factory Operation.insert(dynamic data, [Map? attributes]) => - Operation._(Operation.insertKey, data is String ? data.length : 1, data, - attributes); - - /// Creates operation which retains [length] of characters and optionally - /// applies attributes. - factory Operation.retain(int? length, [Map? attributes]) => - Operation._(Operation.retainKey, length, '', attributes); - /// Returns value of this operation. /// /// For insert operations this returns text, for delete and retain - length. @@ -135,7 +135,7 @@ class Operation { bool operator ==(other) { if (identical(this, other)) return true; if (other is! Operation) return false; - Operation typedOther = other; + final typedOther = other; return key == typedOther.key && length == typedOther.length && _valueEquality.equals(data, typedOther.data) && @@ -180,6 +180,15 @@ class Operation { /// "document delta". When delta includes also "retain" or "delete" operations /// it is a "change delta". class Delta { + /// Creates new empty [Delta]. + factory Delta() => Delta._([]); + + Delta._(List operations) : _operations = operations; + + /// Creates new [Delta] from [other]. + factory Delta.from(Delta other) => + Delta._(List.from(other._operations)); + /// Transforms two attribute sets. static Map? transformAttributes( Map? a, Map? b, bool priority) { @@ -221,14 +230,14 @@ class Delta { attr ??= const {}; base ??= const {}; - var baseInverted = base.keys.fold({}, (dynamic memo, key) { + final baseInverted = base.keys.fold({}, (dynamic memo, key) { if (base![key] != attr![key] && attr.containsKey(key)) { memo[key] = base[key]; } return memo; }); - var inverted = + final inverted = Map.from(attr.keys.fold(baseInverted, (memo, key) { if (base![key] != attr![key] && !base.containsKey(key)) { memo[key] = null; @@ -242,15 +251,6 @@ class Delta { int _modificationCount = 0; - Delta._(List operations) : _operations = operations; - - /// Creates new empty [Delta]. - factory Delta() => Delta._([]); - - /// Creates new [Delta] from [other]. - factory Delta.from(Delta other) => - Delta._(List.from(other._operations)); - /// Creates [Delta] from de-serialized JSON representation. /// /// If `dataDecoder` parameter is not null then it is used to additionally @@ -292,9 +292,8 @@ class Delta { bool operator ==(dynamic other) { if (identical(this, other)) return true; if (other is! Delta) return false; - Delta typedOther = other; - final comparator = - const ListEquality(DefaultEquality()); + final typedOther = other; + const comparator = ListEquality(DefaultEquality()); return comparator.equals(_operations, typedOther._operations); } @@ -529,7 +528,8 @@ class Delta { if (op.isDelete) { inverted.push(baseOp); } else if (op.isRetain && op.isNotPlain) { - var invertAttr = invertAttributes(op.attributes, baseOp.attributes); + final invertAttr = + invertAttributes(op.attributes, baseOp.attributes); inverted.retain( baseOp.length!, invertAttr.isEmpty ? null : invertAttr); } @@ -548,7 +548,7 @@ class Delta { Delta slice(int start, [int? end]) { final delta = Delta(); var index = 0; - var opIterator = DeltaIterator(this); + final opIterator = DeltaIterator(this); final actualEnd = end ?? double.infinity; @@ -599,13 +599,13 @@ class Delta { /// Specialized iterator for [Delta]s. class DeltaIterator { + DeltaIterator(this.delta) : _modificationCount = delta._modificationCount; + final Delta delta; final int _modificationCount; int _index = 0; num _offset = 0; - DeltaIterator(this.delta) : _modificationCount = delta._modificationCount; - bool get isNextInsert => nextOperationKey == Operation.insertKey; bool get isNextDelete => nextOperationKey == Operation.deleteKey; @@ -661,7 +661,7 @@ class DeltaIterator { final opIsNotEmpty = opData is String ? opData.isNotEmpty : true; // embeds are never empty final opLength = opData is String ? opData.length : 1; - final int opActualLength = opIsNotEmpty ? opLength : actualLength as int; + final opActualLength = opIsNotEmpty ? opLength : actualLength as int; return Operation._(opKey, opActualLength, opData, opAttributes); } return Operation.retain(length); diff --git a/lib/models/rules/delete.dart b/lib/models/rules/delete.dart index 15aa3862..e6682f94 100644 --- a/lib/models/rules/delete.dart +++ b/lib/models/rules/delete.dart @@ -1,6 +1,6 @@ -import 'package:flutter_quill/models/documents/attribute.dart'; -import 'package:flutter_quill/models/quill_delta.dart'; -import 'package:flutter_quill/models/rules/rule.dart'; +import '../documents/attribute.dart'; +import '../quill_delta.dart'; +import 'rule.dart'; abstract class DeleteRule extends Rule { const DeleteRule(); @@ -34,34 +34,33 @@ class PreserveLineStyleOnMergeRule extends DeleteRule { @override Delta? applyRule(Delta document, int index, {int? len, Object? data, Attribute? attribute}) { - DeltaIterator itr = DeltaIterator(document); - itr.skip(index); - Operation op = itr.next(1); + final itr = DeltaIterator(document)..skip(index); + var op = itr.next(1); if (op.data != '\n') { return null; } - bool isNotPlain = op.isNotPlain; - Map? attrs = op.attributes; + final isNotPlain = op.isNotPlain; + final attrs = op.attributes; itr.skip(len! - 1); - Delta delta = Delta() + final delta = Delta() ..retain(index) ..delete(len); while (itr.hasNext) { op = itr.next(); - String text = op.data is String ? (op.data as String?)! : ''; - int lineBreak = text.indexOf('\n'); + final text = op.data is String ? (op.data as String?)! : ''; + final lineBreak = text.indexOf('\n'); if (lineBreak == -1) { delta.retain(op.length!); continue; } - Map? attributes = op.attributes == null + var attributes = op.attributes == null ? null - : op.attributes!.map((String key, dynamic value) => - MapEntry(key, null)); + : op.attributes!.map( + (key, dynamic value) => MapEntry(key, null)); if (isNotPlain) { attributes ??= {}; @@ -80,15 +79,15 @@ class EnsureEmbedLineRule extends DeleteRule { @override Delta? applyRule(Delta document, int index, {int? len, Object? data, Attribute? attribute}) { - DeltaIterator itr = DeltaIterator(document); + final itr = DeltaIterator(document); - Operation? op = itr.skip(index); + var op = itr.skip(index); int? indexDelta = 0, lengthDelta = 0, remain = len; - bool embedFound = op != null && op.data is! String; - bool hasLineBreakBefore = + var embedFound = op != null && op.data is! String; + final hasLineBreakBefore = !embedFound && (op == null || (op.data as String).endsWith('\n')); if (embedFound) { - Operation candidate = itr.next(1); + var candidate = itr.next(1); if (remain != null) { remain--; if (candidate.data == '\n') { @@ -107,7 +106,7 @@ class EnsureEmbedLineRule extends DeleteRule { op = itr.skip(remain!); if (op != null && (op.data is String ? op.data as String? : '')!.endsWith('\n')) { - Operation candidate = itr.next(1); + final candidate = itr.next(1); if (candidate.data is! String && !hasLineBreakBefore) { embedFound = true; lengthDelta--; diff --git a/lib/models/rules/format.dart b/lib/models/rules/format.dart index 755f4137..be201925 100644 --- a/lib/models/rules/format.dart +++ b/lib/models/rules/format.dart @@ -1,6 +1,6 @@ -import 'package:flutter_quill/models/documents/attribute.dart'; -import 'package:flutter_quill/models/quill_delta.dart'; -import 'package:flutter_quill/models/rules/rule.dart'; +import '../documents/attribute.dart'; +import '../quill_delta.dart'; +import 'rule.dart'; abstract class FormatRule extends Rule { const FormatRule(); @@ -26,21 +26,20 @@ class ResolveLineFormatRule extends FormatRule { return null; } - Delta delta = Delta()..retain(index); - DeltaIterator itr = DeltaIterator(document); - itr.skip(index); + var delta = Delta()..retain(index); + final itr = DeltaIterator(document)..skip(index); Operation op; - for (int cur = 0; cur < len! && itr.hasNext; cur += op.length!) { + 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!); continue; } - String text = op.data as String; - Delta tmp = Delta(); - int offset = 0; + final text = op.data as String; + final tmp = Delta(); + var offset = 0; - for (int lineBreak = text.indexOf('\n'); + for (var lineBreak = text.indexOf('\n'); lineBreak >= 0; lineBreak = text.indexOf('\n', offset)) { tmp..retain(lineBreak - offset)..retain(1, attribute.toJson()); @@ -52,8 +51,8 @@ class ResolveLineFormatRule extends FormatRule { while (itr.hasNext) { op = itr.next(); - String text = op.data is String ? (op.data as String?)! : ''; - int lineBreak = text.indexOf('\n'); + final text = op.data is String ? (op.data as String?)! : ''; + final lineBreak = text.indexOf('\n'); if (lineBreak < 0) { delta.retain(op.length!); continue; @@ -75,9 +74,9 @@ class FormatLinkAtCaretPositionRule extends FormatRule { return null; } - Delta delta = Delta(); - DeltaIterator itr = DeltaIterator(document); - Operation? before = itr.skip(index), after = itr.next(); + final delta = Delta(); + final itr = DeltaIterator(document); + final before = itr.skip(index), after = itr.next(); int? beg = index, retain = 0; if (before != null && before.hasAttribute(attribute.key)) { beg -= before.length!; @@ -105,20 +104,19 @@ class ResolveInlineFormatRule extends FormatRule { return null; } - Delta delta = Delta()..retain(index); - DeltaIterator itr = DeltaIterator(document); - itr.skip(index); + final delta = Delta()..retain(index); + final itr = DeltaIterator(document)..skip(index); Operation op; - for (int cur = 0; cur < len! && itr.hasNext; cur += op.length!) { + for (var cur = 0; cur < len! && itr.hasNext; cur += op.length!) { op = itr.next(len - cur); - String text = op.data is String ? (op.data as String?)! : ''; - int lineBreak = text.indexOf('\n'); + final text = op.data is String ? (op.data as String?)! : ''; + var lineBreak = text.indexOf('\n'); if (lineBreak < 0) { delta.retain(op.length!, attribute.toJson()); continue; } - int pos = 0; + var pos = 0; while (lineBreak >= 0) { delta..retain(lineBreak - pos, attribute.toJson())..retain(1); pos = lineBreak + 1; diff --git a/lib/models/rules/insert.dart b/lib/models/rules/insert.dart index 5ea7f215..f7d029a4 100644 --- a/lib/models/rules/insert.dart +++ b/lib/models/rules/insert.dart @@ -1,9 +1,10 @@ -import 'package:flutter_quill/models/documents/attribute.dart'; -import 'package:flutter_quill/models/documents/style.dart'; -import 'package:flutter_quill/models/quill_delta.dart'; -import 'package:flutter_quill/models/rules/rule.dart'; import 'package:tuple/tuple.dart'; +import '../documents/attribute.dart'; +import '../documents/style.dart'; +import '../quill_delta.dart'; +import 'rule.dart'; + abstract class InsertRule extends Rule { const InsertRule(); @@ -28,28 +29,28 @@ class PreserveLineStyleOnSplitRule extends InsertRule { return null; } - DeltaIterator itr = DeltaIterator(document); - Operation? before = itr.skip(index); + final itr = DeltaIterator(document); + final before = itr.skip(index); if (before == null || before.data is! String || (before.data as String).endsWith('\n')) { return null; } - Operation after = itr.next(); + final after = itr.next(); if (after.data is! String || (after.data as String).startsWith('\n')) { return null; } final text = after.data as String; - Delta delta = Delta()..retain(index); + final delta = Delta()..retain(index); if (text.contains('\n')) { assert(after.isPlain); delta.insert('\n'); return delta; } - Tuple2 nextNewLine = _getNextNewLine(itr); - Map? attributes = nextNewLine.item1?.attributes; + final nextNewLine = _getNextNewLine(itr); + final attributes = nextNewLine.item1?.attributes; return delta..insert('\n', attributes); } @@ -65,19 +66,18 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { return null; } - DeltaIterator itr = DeltaIterator(document); - itr.skip(index); + final itr = DeltaIterator(document)..skip(index); - Tuple2 nextNewLine = _getNextNewLine(itr); - Style lineStyle = + final nextNewLine = _getNextNewLine(itr); + final lineStyle = Style.fromJson(nextNewLine.item1?.attributes ?? {}); - Attribute? attribute = lineStyle.getBlockExceptHeader(); + final attribute = lineStyle.getBlockExceptHeader(); if (attribute == null) { return null; } - var blockStyle = {attribute.key: attribute.value}; + final blockStyle = {attribute.key: attribute.value}; Map? resetStyle; @@ -85,10 +85,10 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { resetStyle = Attribute.header.toJson(); } - List lines = data.split('\n'); - Delta delta = Delta()..retain(index); - for (int i = 0; i < lines.length; i++) { - String line = lines[i]; + final lines = data.split('\n'); + final delta = Delta()..retain(index); + for (var i = 0; i < lines.length; i++) { + final line = lines[i]; if (line.isNotEmpty) { delta.insert(line); } @@ -100,8 +100,8 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { } if (resetStyle != null) { - delta.retain(nextNewLine.item2!); delta + ..retain(nextNewLine.item2!) ..retain((nextNewLine.item1!.data as String).indexOf('\n')) ..retain(1, resetStyle); } @@ -130,10 +130,9 @@ class AutoExitBlockRule extends InsertRule { return null; } - DeltaIterator itr = DeltaIterator(document); - Operation? prev = itr.skip(index), cur = itr.next(); - Attribute? blockStyle = - Style.fromJson(cur.attributes).getBlockExceptHeader(); + final itr = DeltaIterator(document); + final prev = itr.skip(index), cur = itr.next(); + final blockStyle = Style.fromJson(cur.attributes).getBlockExceptHeader(); if (cur.isPlain || blockStyle == null) { return null; } @@ -145,7 +144,7 @@ class AutoExitBlockRule extends InsertRule { return null; } - Tuple2 nextNewLine = _getNextNewLine(itr); + final nextNewLine = _getNextNewLine(itr); if (nextNewLine.item1 != null && nextNewLine.item1!.attributes != null && Style.fromJson(nextNewLine.item1!.attributes).getBlockExceptHeader() == @@ -154,7 +153,7 @@ class AutoExitBlockRule extends InsertRule { } final attributes = cur.attributes ?? {}; - String k = attributes.keys + final k = attributes.keys .firstWhere((k) => Attribute.blockKeysExceptHeader.contains(k)); attributes[k] = null; // retain(1) should be '\n', set it with no attribute @@ -172,9 +171,8 @@ class ResetLineFormatOnNewLineRule extends InsertRule { return null; } - DeltaIterator itr = DeltaIterator(document); - itr.skip(index); - Operation cur = itr.next(); + final itr = DeltaIterator(document)..skip(index); + final cur = itr.next(); if (cur.data is! String || !(cur.data as String).startsWith('\n')) { return null; } @@ -202,12 +200,12 @@ class InsertEmbedsRule extends InsertRule { return null; } - Delta delta = Delta()..retain(index); - DeltaIterator itr = DeltaIterator(document); - Operation? prev = itr.skip(index), cur = itr.next(); + final delta = Delta()..retain(index); + final itr = DeltaIterator(document); + final prev = itr.skip(index), cur = itr.next(); - String? textBefore = prev?.data is String ? prev!.data as String? : ''; - String textAfter = cur.data is String ? (cur.data as String?)! : ''; + final textBefore = prev?.data is String ? prev!.data as String? : ''; + final textAfter = cur.data is String ? (cur.data as String?)! : ''; final isNewlineBefore = prev == null || textBefore!.endsWith('\n'); final isNewlineAfter = textAfter.startsWith('\n'); @@ -221,7 +219,7 @@ class InsertEmbedsRule extends InsertRule { lineStyle = cur.attributes; } else { while (itr.hasNext) { - Operation op = itr.next(); + final op = itr.next(); if ((op.data is String ? op.data as String? : '')!.contains('\n')) { lineStyle = op.attributes; break; @@ -250,17 +248,17 @@ class ForceNewlineForInsertsAroundEmbedRule extends InsertRule { return null; } - String text = data; - DeltaIterator itr = DeltaIterator(document); + final text = data; + final itr = DeltaIterator(document); final prev = itr.skip(index); final cur = itr.next(); - bool cursorBeforeEmbed = cur.data is! String; - bool cursorAfterEmbed = prev != null && prev.data is! String; + final cursorBeforeEmbed = cur.data is! String; + final cursorAfterEmbed = prev != null && prev.data is! String; if (!cursorBeforeEmbed && !cursorAfterEmbed) { return null; } - Delta delta = Delta()..retain(index); + final delta = Delta()..retain(index); if (cursorBeforeEmbed && !text.endsWith('\n')) { return delta..insert(text)..insert('\n'); } @@ -281,19 +279,19 @@ class AutoFormatLinksRule extends InsertRule { return null; } - DeltaIterator itr = DeltaIterator(document); - Operation? prev = itr.skip(index); + final itr = DeltaIterator(document); + final prev = itr.skip(index); if (prev == null || prev.data is! String) { return null; } try { - String cand = (prev.data as String).split('\n').last.split(' ').last; - Uri link = Uri.parse(cand); + final cand = (prev.data as String).split('\n').last.split(' ').last; + final link = Uri.parse(cand); if (!['https', 'http'].contains(link.scheme)) { return null; } - Map attributes = prev.attributes ?? {}; + final attributes = prev.attributes ?? {}; if (attributes.containsKey(Attribute.link.key)) { return null; @@ -320,16 +318,16 @@ class PreserveInlineStylesRule extends InsertRule { return null; } - DeltaIterator itr = DeltaIterator(document); - Operation? prev = itr.skip(index); + final itr = DeltaIterator(document); + final prev = itr.skip(index); if (prev == null || prev.data is! String || (prev.data as String).contains('\n')) { return null; } - Map? attributes = prev.attributes; - String text = data; + final attributes = prev.attributes; + final text = data; if (attributes == null || !attributes.containsKey(Attribute.link.key)) { return Delta() ..retain(index) @@ -337,13 +335,12 @@ class PreserveInlineStylesRule extends InsertRule { } attributes.remove(Attribute.link.key); - Delta delta = Delta() + final delta = Delta() ..retain(index) ..insert(text, attributes.isEmpty ? null : attributes); - Operation next = itr.next(); + final next = itr.next(); - Map nextAttributes = - next.attributes ?? const {}; + final nextAttributes = next.attributes ?? const {}; if (!nextAttributes.containsKey(Attribute.link.key)) { return delta; } @@ -370,9 +367,9 @@ class CatchAllInsertRule extends InsertRule { Tuple2 _getNextNewLine(DeltaIterator iterator) { Operation op; - for (int skipped = 0; iterator.hasNext; skipped += op.length!) { + for (var skipped = 0; iterator.hasNext; skipped += op.length!) { op = iterator.next(); - int lineBreak = + final lineBreak = (op.data is String ? op.data as String? : '')!.indexOf('\n'); if (lineBreak >= 0) { return Tuple2(op, skipped); diff --git a/lib/models/rules/rule.dart b/lib/models/rules/rule.dart index 70a5aa74..4ee6c278 100644 --- a/lib/models/rules/rule.dart +++ b/lib/models/rules/rule.dart @@ -1,7 +1,6 @@ -import 'package:flutter_quill/models/documents/attribute.dart'; -import 'package:flutter_quill/models/documents/document.dart'; -import 'package:flutter_quill/models/quill_delta.dart'; - +import '../documents/attribute.dart'; +import '../documents/document.dart'; +import '../quill_delta.dart'; import 'delete.dart'; import 'format.dart'; import 'insert.dart'; @@ -27,6 +26,8 @@ abstract class Rule { } class Rules { + Rules(this._rules); + final List _rules; static final Rules _instance = Rules([ const FormatLinkAtCaretPositionRule(), @@ -46,14 +47,12 @@ class Rules { const CatchAllDeleteRule(), ]); - Rules(this._rules); - static Rules getInstance() => _instance; Delta apply(RuleType ruleType, Document document, int index, {int? len, Object? data, Attribute? attribute}) { final delta = document.toDelta(); - for (var rule in _rules) { + for (final rule in _rules) { if (rule.type != ruleType) { continue; } diff --git a/lib/utils/color.dart b/lib/utils/color.dart index 4e206644..93b6e12b 100644 --- a/lib/utils/color.dart +++ b/lib/utils/color.dart @@ -118,8 +118,8 @@ Color stringToColor(String? s) { throw 'Color code not supported'; } - String hex = s.replaceFirst('#', ''); - hex = hex.length == 6 ? 'ff' + hex : hex; - int val = int.parse(hex, radix: 16); + var hex = s.replaceFirst('#', ''); + hex = hex.length == 6 ? 'ff$hex' : hex; + final val = int.parse(hex, radix: 16); return Color(val); } diff --git a/lib/utils/diff_delta.dart b/lib/utils/diff_delta.dart index 127c2d8b..003bae47 100644 --- a/lib/utils/diff_delta.dart +++ b/lib/utils/diff_delta.dart @@ -1,6 +1,6 @@ import 'dart:math' as math; -import 'package:flutter_quill/models/quill_delta.dart'; +import '../models/quill_delta.dart'; const Set WHITE_SPACE = { 0x9, @@ -33,6 +33,8 @@ const Set WHITE_SPACE = { // Diff between two texts - old text and new text class Diff { + Diff(this.start, this.deleted, this.inserted); + // Start index in old text at which changes begin. final int start; @@ -42,8 +44,6 @@ class Diff { // The inserted text final String inserted; - Diff(this.start, this.deleted, this.inserted); - @override String toString() { return 'Diff[$start, "$deleted", "$inserted"]'; @@ -52,17 +52,17 @@ class Diff { /* Get diff operation between old text and new text */ Diff getDiff(String oldText, String newText, int cursorPosition) { - int end = oldText.length; - int delta = newText.length - end; - for (int limit = math.max(0, cursorPosition - delta); + var end = oldText.length; + final delta = newText.length - end; + for (final limit = math.max(0, cursorPosition - delta); end > limit && oldText[end - 1] == newText[end + delta - 1]; end--) {} - int start = 0; - for (int startLimit = cursorPosition - math.max(0, delta); + var start = 0; + for (final startLimit = cursorPosition - math.max(0, delta); start < startLimit && oldText[start] == newText[start]; start++) {} - String deleted = (start >= end) ? '' : oldText.substring(start, end); - String inserted = newText.substring(start, end + delta); + final deleted = (start >= end) ? '' : oldText.substring(start, end); + final inserted = newText.substring(start, end + delta); return Diff(start, deleted, inserted); } @@ -71,19 +71,15 @@ int getPositionDelta(Delta user, Delta actual) { return 0; } - DeltaIterator userItr = DeltaIterator(user); - DeltaIterator actualItr = DeltaIterator(actual); - int diff = 0; + final userItr = DeltaIterator(user); + final actualItr = DeltaIterator(actual); + var diff = 0; while (userItr.hasNext || actualItr.hasNext) { final length = math.min(userItr.peekLength(), actualItr.peekLength()); - Operation userOperation = userItr.next(length as int); - Operation actualOperation = actualItr.next(length); + final userOperation = userItr.next(length as int); + final actualOperation = actualItr.next(length); if (userOperation.length != actualOperation.length) { - throw 'userOp ' + - userOperation.length.toString() + - ' does not match ' + - ' actualOp ' + - actualOperation.length.toString(); + throw 'userOp ${userOperation.length} does not match actualOp ${actualOperation.length}'; } if (userOperation.key == actualOperation.key) { continue; diff --git a/lib/widgets/box.dart b/lib/widgets/box.dart index 5e43b841..75547923 100644 --- a/lib/widgets/box.dart +++ b/lib/widgets/box.dart @@ -1,5 +1,6 @@ import 'package:flutter/rendering.dart'; -import 'package:flutter_quill/models/documents/nodes/container.dart'; + +import '../models/documents/nodes/container.dart'; abstract class RenderContentProxyBox implements RenderBox { double getPreferredLineHeight(); diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index da876bf1..3bd66b6f 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -1,19 +1,16 @@ import 'dart:math' as math; import 'package:flutter/cupertino.dart'; -import 'package:flutter_quill/models/documents/attribute.dart'; -import 'package:flutter_quill/models/documents/document.dart'; -import 'package:flutter_quill/models/documents/nodes/embed.dart'; -import 'package:flutter_quill/models/documents/style.dart'; -import 'package:flutter_quill/models/quill_delta.dart'; -import 'package:flutter_quill/utils/diff_delta.dart'; import 'package:tuple/tuple.dart'; -class QuillController extends ChangeNotifier { - final Document document; - TextSelection selection; - Style toggledStyle = Style(); +import '../models/documents/attribute.dart'; +import '../models/documents/document.dart'; +import '../models/documents/nodes/embed.dart'; +import '../models/documents/style.dart'; +import '../models/quill_delta.dart'; +import '../utils/diff_delta.dart'; +class QuillController extends ChangeNotifier { QuillController({required this.document, required this.selection}); factory QuillController.basic() { @@ -23,6 +20,10 @@ class QuillController extends ChangeNotifier { ); } + final Document document; + TextSelection selection; + Style toggledStyle = Style(); + // item1: Document state before [change]. // // item2: Change delta applied to the document. @@ -42,7 +43,7 @@ class QuillController extends ChangeNotifier { } void undo() { - Tuple2 tup = document.undo(); + final tup = document.undo(); if (tup.item1) { _handleHistoryChange(tup.item2); } @@ -64,7 +65,7 @@ class QuillController extends ChangeNotifier { } void redo() { - Tuple2 tup = document.redo(); + final tup = document.redo(); if (tup.item1) { _handleHistoryChange(tup.item2); } @@ -81,7 +82,7 @@ class QuillController extends ChangeNotifier { Delta? delta; if (len > 0 || data is! String || data.isNotEmpty) { delta = document.replace(index, len, data); - bool shouldRetainDelta = toggledStyle.isNotEmpty && + var shouldRetainDelta = toggledStyle.isNotEmpty && delta.isNotEmpty && delta.length <= 2 && delta.last.isInsert; @@ -97,7 +98,7 @@ class QuillController extends ChangeNotifier { } } if (shouldRetainDelta) { - Delta retainDelta = Delta() + final retainDelta = Delta() ..retain(index) ..retain(data is String ? data.length : 1, toggledStyle.toJson()); document.compose(retainDelta, ChangeSource.LOCAL); @@ -109,11 +110,11 @@ class QuillController extends ChangeNotifier { if (delta == null || delta.isEmpty) { _updateSelection(textSelection, ChangeSource.LOCAL); } else { - Delta user = Delta() + final user = Delta() ..retain(index) ..insert(data) ..delete(len); - int positionDelta = getPositionDelta(user, delta); + final positionDelta = getPositionDelta(user, delta); _updateSelection( textSelection.copyWith( baseOffset: textSelection.baseOffset + positionDelta, @@ -133,8 +134,8 @@ class QuillController extends ChangeNotifier { toggledStyle = toggledStyle.put(attribute); } - Delta change = document.format(index, len, attribute); - TextSelection adjustedSelection = selection.copyWith( + final change = document.format(index, len, attribute); + final adjustedSelection = selection.copyWith( baseOffset: change.transformPosition(selection.baseOffset), extentOffset: change.transformPosition(selection.extentOffset)); if (selection != adjustedSelection) { @@ -176,7 +177,7 @@ class QuillController extends ChangeNotifier { void _updateSelection(TextSelection textSelection, ChangeSource source) { selection = textSelection; - int end = document.length - 1; + final end = document.length - 1; selection = selection.copyWith( baseOffset: math.min(selection.baseOffset, end), extentOffset: math.min(selection.extentOffset, end)); diff --git a/lib/widgets/cursor.dart b/lib/widgets/cursor.dart index 4195d947..383906d0 100644 --- a/lib/widgets/cursor.dart +++ b/lib/widgets/cursor.dart @@ -8,15 +8,6 @@ import 'box.dart'; const Duration _FADE_DURATION = Duration(milliseconds: 250); class CursorStyle { - final Color color; - final Color backgroundColor; - final double width; - final double? height; - final Radius? radius; - final Offset? offset; - final bool opacityAnimates; - final bool paintAboveText; - const CursorStyle({ required this.color, required this.backgroundColor, @@ -28,6 +19,15 @@ class CursorStyle { this.paintAboveText = false, }); + final Color color; + final Color backgroundColor; + final double width; + final double? height; + final Radius? radius; + final Offset? offset; + final bool opacityAnimates; + final bool paintAboveText; + @override bool operator ==(Object other) => identical(this, other) || @@ -55,20 +55,11 @@ class CursorStyle { } class CursorCont extends ChangeNotifier { - final ValueNotifier show; - final ValueNotifier _blink; - final ValueNotifier color; - late AnimationController _blinkOpacityCont; - Timer? _cursorTimer; - bool _targetCursorVisibility = false; - CursorStyle _style; - CursorCont({ - required ValueNotifier show, + required this.show, required CursorStyle style, required TickerProvider tickerProvider, - }) : show = show, - _style = style, + }) : _style = style, _blink = ValueNotifier(false), color = ValueNotifier(style.color) { _blinkOpacityCont = @@ -76,6 +67,14 @@ class CursorCont extends ChangeNotifier { _blinkOpacityCont.addListener(_onColorTick); } + final ValueNotifier show; + final ValueNotifier _blink; + final ValueNotifier color; + late AnimationController _blinkOpacityCont; + Timer? _cursorTimer; + bool _targetCursorVisibility = false; + CursorStyle _style; + ValueNotifier get cursorBlink => _blink; ValueNotifier get cursorColor => color; @@ -99,7 +98,7 @@ class CursorCont extends ChangeNotifier { void _cursorTick(Timer timer) { _targetCursorVisibility = !_targetCursorVisibility; - double targetOpacity = _targetCursorVisibility ? 1.0 : 0.0; + final targetOpacity = _targetCursorVisibility ? 1.0 : 0.0; if (style.opacityAnimates) { _blinkOpacityCont.animateTo(targetOpacity, curve: Curves.easeOut); } else { @@ -133,8 +132,9 @@ class CursorCont extends ChangeNotifier { _blinkOpacityCont.value = 0.0; if (style.opacityAnimates) { - _blinkOpacityCont.stop(); - _blinkOpacityCont.value = 0.0; + _blinkOpacityCont + ..stop() + ..value = 0.0; } } @@ -156,30 +156,30 @@ class CursorCont extends ChangeNotifier { } class CursorPainter { + CursorPainter(this.editable, this.style, this.prototype, this.color, + this.devicePixelRatio); + final RenderContentProxyBox? editable; final CursorStyle style; final Rect? prototype; final Color color; final double devicePixelRatio; - CursorPainter(this.editable, this.style, this.prototype, this.color, - this.devicePixelRatio); - void paint(Canvas canvas, Offset offset, TextPosition position) { assert(prototype != null); - Offset caretOffset = + final caretOffset = editable!.getOffsetForCaret(position, prototype) + offset; - Rect caretRect = prototype!.shift(caretOffset); + var caretRect = prototype!.shift(caretOffset); if (style.offset != null) { caretRect = caretRect.shift(style.offset!); } if (caretRect.left < 0.0) { - caretRect = caretRect.shift(Offset(-caretRect.left, 0.0)); + caretRect = caretRect.shift(Offset(-caretRect.left, 0)); } - double? caretHeight = editable!.getFullHeightForCaret(position); + final caretHeight = editable!.getFullHeightForCaret(position); if (caretHeight != null) { switch (defaultTargetPlatform) { case TargetPlatform.android: @@ -207,8 +207,8 @@ class CursorPainter { } } - Offset caretPosition = editable!.localToGlobal(caretRect.topLeft); - double pixelMultiple = 1.0 / devicePixelRatio; + final caretPosition = editable!.localToGlobal(caretRect.topLeft); + final pixelMultiple = 1.0 / devicePixelRatio; caretRect = caretRect.shift(Offset( caretPosition.dx.isFinite ? (caretPosition.dx / pixelMultiple).round() * pixelMultiple - @@ -219,13 +219,13 @@ class CursorPainter { caretPosition.dy : caretPosition.dy)); - Paint paint = Paint()..color = color; + final paint = Paint()..color = color; if (style.radius == null) { canvas.drawRect(caretRect, paint); return; } - RRect caretRRect = RRect.fromRectAndRadius(caretRect, style.radius!); + final caretRRect = RRect.fromRectAndRadius(caretRect, style.radius!); canvas.drawRRect(caretRRect, paint); } } diff --git a/lib/widgets/default_styles.dart b/lib/widgets/default_styles.dart index 9490bebd..1cebe135 100644 --- a/lib/widgets/default_styles.dart +++ b/lib/widgets/default_styles.dart @@ -3,21 +3,21 @@ import 'package:flutter/widgets.dart'; import 'package:tuple/tuple.dart'; class QuillStyles extends InheritedWidget { - final DefaultStyles data; - const QuillStyles({ required this.data, required Widget child, Key? key, }) : super(key: key, child: child); + final DefaultStyles data; + @override bool updateShouldNotify(QuillStyles oldWidget) { return data != oldWidget.data; } static DefaultStyles? getStyles(BuildContext context, bool nullOk) { - var widget = context.dependOnInheritedWidgetOfExactType(); + final widget = context.dependOnInheritedWidgetOfExactType(); if (widget == null && nullOk) { return null; } @@ -27,6 +27,13 @@ class QuillStyles extends InheritedWidget { } class DefaultTextBlockStyle { + DefaultTextBlockStyle( + this.style, + this.verticalSpacing, + this.lineSpacing, + this.decoration, + ); + final TextStyle style; final Tuple2 verticalSpacing; @@ -34,12 +41,32 @@ class DefaultTextBlockStyle { final Tuple2 lineSpacing; final BoxDecoration? decoration; - - DefaultTextBlockStyle( - this.style, this.verticalSpacing, this.lineSpacing, this.decoration); } class DefaultStyles { + DefaultStyles({ + this.h1, + this.h2, + this.h3, + this.paragraph, + this.bold, + this.italic, + this.underline, + this.strikeThrough, + this.link, + this.color, + this.placeHolder, + this.lists, + this.quote, + this.code, + this.indent, + this.align, + this.leading, + this.sizeSmall, + this.sizeLarge, + this.sizeHuge, + }); + final DefaultTextBlockStyle? h1; final DefaultTextBlockStyle? h2; final DefaultTextBlockStyle? h3; @@ -61,36 +88,14 @@ class DefaultStyles { final DefaultTextBlockStyle? align; final DefaultTextBlockStyle? leading; - DefaultStyles( - {this.h1, - this.h2, - this.h3, - this.paragraph, - this.bold, - this.italic, - this.underline, - this.strikeThrough, - this.link, - this.color, - this.placeHolder, - this.lists, - this.quote, - this.code, - this.indent, - this.align, - this.leading, - this.sizeSmall, - this.sizeLarge, - this.sizeHuge}); - static DefaultStyles getInstance(BuildContext context) { - ThemeData themeData = Theme.of(context); - DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); - TextStyle baseStyle = defaultTextStyle.style.copyWith( - fontSize: 16.0, + final themeData = Theme.of(context); + final defaultTextStyle = DefaultTextStyle.of(context); + final baseStyle = defaultTextStyle.style.copyWith( + fontSize: 16, height: 1.3, ); - Tuple2 baseSpacing = const Tuple2(6.0, 0); + const baseSpacing = Tuple2(6, 0); String fontFamily; switch (themeData.platform) { case TargetPlatform.iOS: @@ -110,36 +115,36 @@ class DefaultStyles { return DefaultStyles( h1: DefaultTextBlockStyle( defaultTextStyle.style.copyWith( - fontSize: 34.0, + fontSize: 34, color: defaultTextStyle.style.color!.withOpacity(0.70), height: 1.15, fontWeight: FontWeight.w300, ), - const Tuple2(16.0, 0.0), - const Tuple2(0.0, 0.0), + const Tuple2(16, 0), + const Tuple2(0, 0), null), h2: DefaultTextBlockStyle( defaultTextStyle.style.copyWith( - fontSize: 24.0, + fontSize: 24, color: defaultTextStyle.style.color!.withOpacity(0.70), height: 1.15, fontWeight: FontWeight.normal, ), - const Tuple2(8.0, 0.0), - const Tuple2(0.0, 0.0), + const Tuple2(8, 0), + const Tuple2(0, 0), null), h3: DefaultTextBlockStyle( defaultTextStyle.style.copyWith( - fontSize: 20.0, + fontSize: 20, color: defaultTextStyle.style.color!.withOpacity(0.70), height: 1.25, fontWeight: FontWeight.w500, ), - const Tuple2(8.0, 0.0), - const Tuple2(0.0, 0.0), + const Tuple2(8, 0), + const Tuple2(0, 0), null), paragraph: DefaultTextBlockStyle( - baseStyle, const Tuple2(0.0, 0.0), const Tuple2(0.0, 0.0), null), + baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null), bold: const TextStyle(fontWeight: FontWeight.bold), italic: const TextStyle(fontStyle: FontStyle.italic), underline: const TextStyle(decoration: TextDecoration.underline), @@ -150,19 +155,19 @@ class DefaultStyles { ), placeHolder: DefaultTextBlockStyle( defaultTextStyle.style.copyWith( - fontSize: 20.0, + fontSize: 20, height: 1.5, color: Colors.grey.withOpacity(0.6), ), - const Tuple2(0.0, 0.0), - const Tuple2(0.0, 0.0), + const Tuple2(0, 0), + const Tuple2(0, 0), null), lists: DefaultTextBlockStyle( - baseStyle, baseSpacing, const Tuple2(0.0, 6.0), null), + baseStyle, baseSpacing, const Tuple2(0, 6), null), quote: DefaultTextBlockStyle( TextStyle(color: baseStyle.color!.withOpacity(0.6)), baseSpacing, - const Tuple2(6.0, 2.0), + const Tuple2(6, 2), BoxDecoration( border: Border( left: BorderSide(width: 4, color: Colors.grey.shade300), @@ -172,24 +177,24 @@ class DefaultStyles { TextStyle( color: Colors.blue.shade900.withOpacity(0.9), fontFamily: fontFamily, - fontSize: 13.0, + fontSize: 13, height: 1.15, ), baseSpacing, - const Tuple2(0.0, 0.0), + const Tuple2(0, 0), BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(2), )), indent: DefaultTextBlockStyle( - baseStyle, baseSpacing, const Tuple2(0.0, 6.0), null), + baseStyle, baseSpacing, const Tuple2(0, 6), null), align: DefaultTextBlockStyle( - baseStyle, const Tuple2(0.0, 0.0), const Tuple2(0.0, 0.0), null), + baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null), leading: DefaultTextBlockStyle( - baseStyle, const Tuple2(0.0, 0.0), const Tuple2(0.0, 0.0), null), - sizeSmall: const TextStyle(fontSize: 10.0), - sizeLarge: const TextStyle(fontSize: 18.0), - sizeHuge: const TextStyle(fontSize: 22.0)); + baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null), + sizeSmall: const TextStyle(fontSize: 10), + sizeLarge: const TextStyle(fontSize: 18), + sizeHuge: const TextStyle(fontSize: 22)); } DefaultStyles merge(DefaultStyles other) { diff --git a/lib/widgets/delegate.dart b/lib/widgets/delegate.dart index 3cfb9cd6..4b4bdea7 100644 --- a/lib/widgets/delegate.dart +++ b/lib/widgets/delegate.dart @@ -2,10 +2,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter_quill/models/documents/nodes/leaf.dart'; -import 'package:flutter_quill/widgets/text_selection.dart'; +import '../models/documents/nodes/leaf.dart'; import 'editor.dart'; +import 'text_selection.dart'; typedef EmbedBuilder = Widget Function(BuildContext context, Embed node); @@ -18,11 +18,11 @@ abstract class EditorTextSelectionGestureDetectorBuilderDelegate { } class EditorTextSelectionGestureDetectorBuilder { + EditorTextSelectionGestureDetectorBuilder(this.delegate); + final EditorTextSelectionGestureDetectorBuilderDelegate delegate; bool shouldShowSelectionToolbar = true; - EditorTextSelectionGestureDetectorBuilder(this.delegate); - EditorState? getEditor() { return delegate.getEditableTextKey().currentState; } @@ -34,7 +34,7 @@ class EditorTextSelectionGestureDetectorBuilder { void onTapDown(TapDownDetails details) { getRenderEditor()!.handleTapDown(details); - PointerDeviceKind? kind = details.kind; + final kind = details.kind; shouldShowSelectionToolbar = kind == null || kind == PointerDeviceKind.touch || kind == PointerDeviceKind.stylus; diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index ccb2eb38..d11653a1 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -8,25 +8,23 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_quill/models/documents/attribute.dart'; -import 'package:flutter_quill/models/documents/document.dart'; -import 'package:flutter_quill/models/documents/nodes/container.dart' - as container_node; -import 'package:flutter_quill/models/documents/nodes/embed.dart'; -import 'package:flutter_quill/models/documents/nodes/leaf.dart' as leaf; -import 'package:flutter_quill/models/documents/nodes/line.dart'; -import 'package:flutter_quill/models/documents/nodes/node.dart'; -import 'package:flutter_quill/widgets/image.dart'; -import 'package:flutter_quill/widgets/raw_editor.dart'; -import 'package:flutter_quill/widgets/text_selection.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'; +import '../models/documents/nodes/leaf.dart' as leaf; +import '../models/documents/nodes/line.dart'; import 'box.dart'; import 'controller.dart'; import 'cursor.dart'; import 'default_styles.dart'; import 'delegate.dart'; +import 'image.dart'; +import 'raw_editor.dart'; +import 'text_selection.dart'; const linkPrefixes = [ 'mailto:', // email @@ -101,7 +99,7 @@ Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) { assert(!kIsWeb, 'Please provide EmbedBuilder for Web'); switch (node.value.type) { case 'image': - String imageUrl = _standardizeImageUrl(node.value.data); + final imageUrl = _standardizeImageUrl(node.value.data); return imageUrl.startsWith('http') ? Image.network(imageUrl) : isBase64(imageUrl) @@ -116,6 +114,43 @@ Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) { } class QuillEditor extends StatefulWidget { + const QuillEditor({ + required this.controller, + required this.focusNode, + required this.scrollController, + required this.scrollable, + required this.padding, + required this.autoFocus, + required this.readOnly, + required this.expands, + this.showCursor, + this.placeholder, + this.enableInteractiveSelection = true, + this.minHeight, + this.maxHeight, + this.customStyles, + this.textCapitalization = TextCapitalization.sentences, + this.keyboardAppearance = Brightness.light, + this.scrollPhysics, + this.onLaunchUrl, + this.embedBuilder = _defaultEmbedBuilder, + }); + + factory QuillEditor.basic({ + required QuillController controller, + required bool readOnly, + }) { + return QuillEditor( + controller: controller, + scrollController: ScrollController(), + scrollable: true, + focusNode: FocusNode(), + autoFocus: true, + readOnly: readOnly, + expands: false, + padding: EdgeInsets.zero); + } + final QuillController controller; final FocusNode focusNode; final ScrollController scrollController; @@ -146,45 +181,6 @@ class QuillEditor extends StatefulWidget { final bool Function(LongPressEndDetails details, TextPosition textPosition)? onSingleLongTapEnd; final EmbedBuilder embedBuilder; - const QuillEditor( - {required this.controller, - required this.focusNode, - required this.scrollController, - required this.scrollable, - required this.padding, - required this.autoFocus, - required this.readOnly, - required this.expands, - this.showCursor, - this.placeholder, - this.enableInteractiveSelection = true, - this.minHeight, - this.maxHeight, - this.customStyles, - this.textCapitalization = TextCapitalization.sentences, - this.keyboardAppearance = Brightness.light, - this.scrollPhysics, - this.onLaunchUrl, - this.onTapDown, - this.onTapUp, - this.onSingleLongTapStart, - this.onSingleLongTapMoveUpdate, - this.onSingleLongTapEnd, - this.embedBuilder = _defaultEmbedBuilder}); - - factory QuillEditor.basic( - {required QuillController controller, required bool readOnly}) { - return QuillEditor( - controller: controller, - scrollController: ScrollController(), - scrollable: true, - focusNode: FocusNode(), - autoFocus: true, - readOnly: readOnly, - expands: false, - padding: EdgeInsets.zero); - } - @override _QuillEditorState createState() => _QuillEditorState(); } @@ -204,8 +200,8 @@ class _QuillEditorState extends State @override Widget build(BuildContext context) { - ThemeData theme = Theme.of(context); - TextSelectionThemeData selectionTheme = TextSelectionTheme.of(context); + final theme = Theme.of(context); + final selectionTheme = TextSelectionTheme.of(context); TextSelectionControls textSelectionControls; bool paintCursorAboveText; @@ -229,7 +225,7 @@ class _QuillEditorState extends State break; case TargetPlatform.iOS: case TargetPlatform.macOS: - CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); + final cupertinoTheme = CupertinoTheme.of(context); textSelectionControls = cupertinoTextSelectionControls; paintCursorAboveText = true; cursorOpacityAnimates = true; @@ -237,7 +233,7 @@ class _QuillEditorState extends State selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; selectionColor = selectionTheme.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40); - cursorRadius ??= const Radius.circular(2.0); + cursorRadius ??= const Radius.circular(2); cursorOffset = Offset( iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); break; @@ -269,7 +265,7 @@ class _QuillEditorState extends State CursorStyle( color: cursorColor, backgroundColor: Colors.grey, - width: 2.0, + width: 2, radius: cursorRadius, offset: cursorOffset, paintAboveText: paintCursorAboveText, @@ -286,6 +282,11 @@ class _QuillEditorState extends State widget.keyboardAppearance, widget.enableInteractiveSelection, widget.scrollPhysics, + widget.onTapDown, + widget.onTapUp, + widget.onSingleLongTapStart, + widget.onSingleLongTapMoveUpdate, + widget.onSingleLongTapEnd, widget.embedBuilder), ); } @@ -312,10 +313,10 @@ class _QuillEditorState extends State class _QuillEditorSelectionGestureDetectorBuilder extends EditorTextSelectionGestureDetectorBuilder { - final _QuillEditorState _state; - _QuillEditorSelectionGestureDetectorBuilder(this._state) : super(_state); + final _QuillEditorState _state; + @override void onForcePressStart(ForcePressDetails details) { super.onForcePressStart(details); @@ -369,16 +370,14 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.controller.document.isEmpty()) { return false; } - TextPosition pos = - getRenderEditor()!.getPositionForOffset(details.globalPosition); - container_node.ChildQuery result = + final pos = getRenderEditor()!.getPositionForOffset(details.globalPosition); + final result = getEditor()!.widget.controller.document.queryChild(pos.offset); if (result.node == null) { return false; } - Line line = result.node as Line; - container_node.ChildQuery segmentResult = - line.queryChild(result.offset, false); + final line = result.node as Line; + final segmentResult = line.queryChild(result.offset, false); if (segmentResult.node == null) { if (line.length == 1) { // tapping when no text yet on this line @@ -389,7 +388,7 @@ class _QuillEditorSelectionGestureDetectorBuilder } return false; } - leaf.Leaf segment = segmentResult.node as leaf.Leaf; + final segment = segmentResult.node as leaf.Leaf; if (segment.style.containsKey(Attribute.link.key)) { var launchUrl = getEditor()!.widget.onLaunchUrl; launchUrl ??= _launchUrl; @@ -405,9 +404,9 @@ class _QuillEditorSelectionGestureDetectorBuilder return false; } if (getEditor()!.widget.readOnly && segment.value is BlockEmbed) { - BlockEmbed blockEmbed = segment.value as BlockEmbed; + final blockEmbed = segment.value as BlockEmbed; if (blockEmbed.type == 'image') { - final String imageUrl = _standardizeImageUrl(blockEmbed.data); + final imageUrl = _standardizeImageUrl(blockEmbed.data); Navigator.push( getEditor()!.context, MaterialPageRoute( @@ -438,7 +437,7 @@ class _QuillEditorSelectionGestureDetectorBuilder return false; } // segmentResult.offset == 0 means tap at the beginning of the TextLine - String? listVal = line.style.attributes[Attribute.list.key]!.value; + final String? listVal = line.style.attributes[Attribute.list.key]!.value; if (listVal == Attribute.unchecked.value) { getEditor()! .widget @@ -455,7 +454,7 @@ class _QuillEditorSelectionGestureDetectorBuilder return true; } - void _launchUrl(String url) async { + Future _launchUrl(String url) async { await launch(url); } @@ -485,7 +484,7 @@ class _QuillEditorSelectionGestureDetectorBuilder getEditor()!.hideToolbar(); - bool positionSelected = _onTapping(details); + final positionSelected = _onTapping(details); if (delegate.getSelectionEnabled() && !positionSelected) { switch (Theme.of(_state.context).platform) { @@ -567,6 +566,24 @@ typedef TextSelectionChangedHandler = void Function( class RenderEditor extends RenderEditableContainerBox implements RenderAbstractEditor { + RenderEditor( + List? children, + TextDirection textDirection, + EdgeInsetsGeometry padding, + this.document, + this.selection, + this._hasFocus, + this.onSelectionChanged, + this._startHandleLayerLink, + this._endHandleLayerLink, + EdgeInsets floatingCursorAddedMargin, + ) : super( + children, + document.root, + textDirection, + padding, + ); + Document document; TextSelection selection; bool _hasFocus = false; @@ -582,24 +599,6 @@ class RenderEditor extends RenderEditableContainerBox ValueListenable get selectionEndInViewport => _selectionEndInViewport; final ValueNotifier _selectionEndInViewport = ValueNotifier(true); - RenderEditor( - List? children, - TextDirection textDirection, - EdgeInsetsGeometry padding, - this.document, - this.selection, - this._hasFocus, - this.onSelectionChanged, - this._startHandleLayerLink, - this._endHandleLayerLink, - EdgeInsets floatingCursorAddedMargin) - : super( - children, - document.root, - textDirection, - padding, - ); - void setDocument(Document doc) { if (document == doc) { return; @@ -644,22 +643,21 @@ class RenderEditor extends RenderEditableContainerBox List getEndpointsForSelection( TextSelection textSelection) { if (textSelection.isCollapsed) { - RenderEditableBox child = childAtPosition(textSelection.extent); - TextPosition localPosition = TextPosition( - offset: - textSelection.extentOffset - child.getContainer().getOffset()); - Offset localOffset = child.getOffsetForCaret(localPosition); - BoxParentData parentData = child.parentData as BoxParentData; + final child = childAtPosition(textSelection.extent); + final localPosition = TextPosition( + offset: textSelection.extentOffset - child.getContainer().offset); + final localOffset = child.getOffsetForCaret(localPosition); + final parentData = child.parentData as BoxParentData; return [ TextSelectionPoint( - Offset(0.0, child.preferredLineHeight(localPosition)) + + Offset(0, child.preferredLineHeight(localPosition)) + localOffset + parentData.offset, null) ]; } - Node? baseNode = _container.queryChild(textSelection.start, false).node; + final baseNode = _container.queryChild(textSelection.start, false).node; var baseChild = firstChild; while (baseChild != null) { @@ -670,15 +668,14 @@ class RenderEditor extends RenderEditableContainerBox } assert(baseChild != null); - BoxParentData baseParentData = baseChild!.parentData as BoxParentData; - TextSelection baseSelection = + final baseParentData = baseChild!.parentData as BoxParentData; + final baseSelection = localSelection(baseChild.getContainer(), textSelection, true); - TextSelectionPoint basePoint = - baseChild.getBaseEndpointForSelection(baseSelection); + var basePoint = baseChild.getBaseEndpointForSelection(baseSelection); basePoint = TextSelectionPoint( basePoint.point + baseParentData.offset, basePoint.direction); - Node? extentNode = _container.queryChild(textSelection.end, false).node; + final extentNode = _container.queryChild(textSelection.end, false).node; RenderEditableBox? extentChild = baseChild; while (extentChild != null) { if (extentChild.getContainer() == extentNode) { @@ -688,10 +685,10 @@ class RenderEditor extends RenderEditableContainerBox } assert(extentChild != null); - BoxParentData extentParentData = extentChild!.parentData as BoxParentData; - TextSelection extentSelection = + final extentParentData = extentChild!.parentData as BoxParentData; + final extentSelection = localSelection(extentChild.getContainer(), textSelection, true); - TextSelectionPoint extentPoint = + var extentPoint = extentChild.getExtentEndpointForSelection(extentSelection); extentPoint = TextSelectionPoint( extentPoint.point + extentParentData.offset, extentPoint.direction); @@ -712,9 +709,9 @@ class RenderEditor extends RenderEditableContainerBox Offset? to, SelectionChangedCause cause, ) { - TextPosition firstPosition = getPositionForOffset(from); - TextSelection firstWord = selectWordAtPosition(firstPosition); - TextSelection lastWord = + final firstPosition = getPositionForOffset(from); + final firstWord = selectWordAtPosition(firstPosition); + final lastWord = to == null ? firstWord : selectWordAtPosition(getPositionForOffset(to)); _handleSelectionChange( @@ -731,7 +728,7 @@ class RenderEditor extends RenderEditableContainerBox TextSelection nextSelection, SelectionChangedCause cause, ) { - bool focusingEmpty = nextSelection.baseOffset == 0 && + final focusingEmpty = nextSelection.baseOffset == 0 && nextSelection.extentOffset == 0 && !_hasFocus; if (nextSelection == selection && @@ -745,15 +742,15 @@ class RenderEditor extends RenderEditableContainerBox @override void selectWordEdge(SelectionChangedCause cause) { assert(_lastTapDownPosition != null); - TextPosition position = getPositionForOffset(_lastTapDownPosition!); - RenderEditableBox child = childAtPosition(position); - int nodeOffset = child.getContainer().getOffset(); - TextPosition localPosition = TextPosition( + final position = getPositionForOffset(_lastTapDownPosition!); + final child = childAtPosition(position); + final nodeOffset = child.getContainer().offset; + final localPosition = TextPosition( offset: position.offset - nodeOffset, affinity: position.affinity, ); - TextRange localWord = child.getWordBoundary(localPosition); - TextRange word = TextRange( + final localWord = child.getWordBoundary(localPosition); + final word = TextRange( start: localWord.start + nodeOffset, end: localWord.end + nodeOffset, ); @@ -777,17 +774,17 @@ class RenderEditor extends RenderEditableContainerBox Offset? to, SelectionChangedCause cause, ) { - TextPosition fromPosition = getPositionForOffset(from); - TextPosition? toPosition = to == null ? null : getPositionForOffset(to); + final fromPosition = getPositionForOffset(from); + final toPosition = to == null ? null : getPositionForOffset(to); - int baseOffset = fromPosition.offset; - int extentOffset = fromPosition.offset; + var baseOffset = fromPosition.offset; + var extentOffset = fromPosition.offset; if (toPosition != null) { baseOffset = math.min(fromPosition.offset, toPosition.offset); extentOffset = math.max(fromPosition.offset, toPosition.offset); } - TextSelection newSelection = TextSelection( + final newSelection = TextSelection( baseOffset: baseOffset, extentOffset: extentOffset, affinity: fromPosition.affinity, @@ -807,12 +804,12 @@ class RenderEditor extends RenderEditableContainerBox @override TextSelection selectWordAtPosition(TextPosition position) { - RenderEditableBox child = childAtPosition(position); - int nodeOffset = child.getContainer().getOffset(); - TextPosition localPosition = TextPosition( + final child = childAtPosition(position); + final nodeOffset = child.getContainer().offset; + final localPosition = TextPosition( offset: position.offset - nodeOffset, affinity: position.affinity); - TextRange localWord = child.getWordBoundary(localPosition); - TextRange word = TextRange( + final localWord = child.getWordBoundary(localPosition); + final word = TextRange( start: localWord.start + nodeOffset, end: localWord.end + nodeOffset, ); @@ -824,12 +821,12 @@ class RenderEditor extends RenderEditableContainerBox @override TextSelection selectLineAtPosition(TextPosition position) { - RenderEditableBox child = childAtPosition(position); - int nodeOffset = child.getContainer().getOffset(); - TextPosition localPosition = TextPosition( + final child = childAtPosition(position); + final nodeOffset = child.getContainer().offset; + final localPosition = TextPosition( offset: position.offset - nodeOffset, affinity: position.affinity); - TextRange localLineRange = child.getLineBoundary(localPosition); - TextRange line = TextRange( + final localLineRange = child.getLineBoundary(localPosition); + final line = TextRange( start: localLineRange.start + nodeOffset, end: localLineRange.end + nodeOffset, ); @@ -879,21 +876,21 @@ class RenderEditor extends RenderEditableContainerBox @override double preferredLineHeight(TextPosition position) { - RenderEditableBox child = childAtPosition(position); - return child.preferredLineHeight(TextPosition( - offset: position.offset - child.getContainer().getOffset())); + final child = childAtPosition(position); + return child.preferredLineHeight( + TextPosition(offset: position.offset - child.getContainer().offset)); } @override TextPosition getPositionForOffset(Offset offset) { - Offset local = globalToLocal(offset); - RenderEditableBox child = childAtOffset(local)!; + final local = globalToLocal(offset); + final child = childAtOffset(local)!; - BoxParentData parentData = child.parentData as BoxParentData; - Offset localOffset = local - parentData.offset; - TextPosition localPosition = child.getPositionForOffset(localOffset); + final parentData = child.parentData as BoxParentData; + final localOffset = local - parentData.offset; + final localPosition = child.getPositionForOffset(localOffset); return TextPosition( - offset: localPosition.offset + child.getContainer().getOffset(), + offset: localPosition.offset + child.getContainer().offset, affinity: localPosition.affinity, ); } @@ -905,15 +902,14 @@ class RenderEditor extends RenderEditableContainerBox /// Returns null if [selection] is already visible. double? getOffsetToRevealCursor( double viewportHeight, double scrollOffset, double offsetInViewport) { - List endpoints = getEndpointsForSelection(selection); - TextSelectionPoint endpoint = endpoints.first; - RenderEditableBox child = childAtPosition(selection.extent); + final endpoints = getEndpointsForSelection(selection); + final endpoint = endpoints.first; + final child = childAtPosition(selection.extent); const kMargin = 8.0; - double caretTop = endpoint.point.dy - + final caretTop = endpoint.point.dy - child.preferredLineHeight(TextPosition( - offset: - selection.extentOffset - child.getContainer().getOffset())) - + offset: selection.extentOffset - child.getContainer().offset)) - kMargin + offsetInViewport; final caretBottom = endpoint.point.dy + kMargin + offsetInViewport; @@ -926,7 +922,7 @@ class RenderEditor extends RenderEditableContainerBox if (dy == null) { return null; } - return math.max(dy, 0.0); + return math.max(dy, 0); } } @@ -939,17 +935,20 @@ class RenderEditableContainerBox extends RenderBox EditableContainerParentData>, RenderBoxContainerDefaultsMixin { + RenderEditableContainerBox( + List? children, + this._container, + this.textDirection, + this._padding, + ) : assert(_padding.isNonNegative) { + addAll(children); + } + container_node.Container _container; TextDirection textDirection; EdgeInsetsGeometry _padding; EdgeInsets? _resolvedPadding; - RenderEditableContainerBox(List? children, this._container, - this.textDirection, this._padding) - : assert(_padding.isNonNegative) { - addAll(children); - } - container_node.Container getContainer() { return _container; } @@ -988,7 +987,7 @@ class RenderEditableContainerBox extends RenderBox RenderEditableBox childAtPosition(TextPosition position) { assert(firstChild != null); - Node? targetNode = _container.queryChild(position.offset, false).node; + final targetNode = _container.queryChild(position.offset, false).node; var targetChild = firstChild; while (targetChild != null) { @@ -1020,7 +1019,8 @@ class RenderEditableContainerBox extends RenderBox } var child = firstChild; - double dx = -offset.dx, dy = _resolvedPadding!.top; + final dx = -offset.dx; + var dy = _resolvedPadding!.top; while (child != null) { if (child.size.contains(offset.translate(dx, -dy))) { return child; @@ -1047,16 +1047,15 @@ class RenderEditableContainerBox extends RenderBox _resolvePadding(); assert(_resolvedPadding != null); - double mainAxisExtent = _resolvedPadding!.top; + var mainAxisExtent = _resolvedPadding!.top; var child = firstChild; - BoxConstraints innerConstraints = + final innerConstraints = BoxConstraints.tightFor(width: constraints.maxWidth) .deflate(_resolvedPadding!); while (child != null) { child.layout(innerConstraints, parentUsesSize: true); - final EditableContainerParentData childParentData = - child.parentData as EditableContainerParentData; - childParentData.offset = Offset(_resolvedPadding!.left, mainAxisExtent); + final childParentData = (child.parentData as EditableContainerParentData) + ..offset = Offset(_resolvedPadding!.left, mainAxisExtent); mainAxisExtent += child.size.height; assert(child.parentData == childParentData); child = childParentData.nextSibling; @@ -1068,24 +1067,22 @@ class RenderEditableContainerBox extends RenderBox } double _getIntrinsicCrossAxis(double Function(RenderBox child) childSize) { - double extent = 0.0; + var extent = 0.0; var child = firstChild; while (child != null) { extent = math.max(extent, childSize(child)); - EditableContainerParentData childParentData = - child.parentData as EditableContainerParentData; + final childParentData = child.parentData as EditableContainerParentData; child = childParentData.nextSibling; } return extent; } double _getIntrinsicMainAxis(double Function(RenderBox child) childSize) { - double extent = 0.0; + var extent = 0.0; var child = firstChild; while (child != null) { extent += childSize(child); - EditableContainerParentData childParentData = - child.parentData as EditableContainerParentData; + final childParentData = child.parentData as EditableContainerParentData; child = childParentData.nextSibling; } return extent; @@ -1094,9 +1091,9 @@ class RenderEditableContainerBox extends RenderBox @override double computeMinIntrinsicWidth(double height) { _resolvePadding(); - return _getIntrinsicCrossAxis((RenderBox child) { - double childHeight = math.max( - 0.0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); + return _getIntrinsicCrossAxis((child) { + final childHeight = math.max( + 0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); return child.getMinIntrinsicWidth(childHeight) + _resolvedPadding!.left + _resolvedPadding!.right; @@ -1106,9 +1103,9 @@ class RenderEditableContainerBox extends RenderBox @override double computeMaxIntrinsicWidth(double height) { _resolvePadding(); - return _getIntrinsicCrossAxis((RenderBox child) { - double childHeight = math.max( - 0.0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); + return _getIntrinsicCrossAxis((child) { + final childHeight = math.max( + 0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); return child.getMaxIntrinsicWidth(childHeight) + _resolvedPadding!.left + _resolvedPadding!.right; @@ -1118,9 +1115,9 @@ class RenderEditableContainerBox extends RenderBox @override double computeMinIntrinsicHeight(double width) { _resolvePadding(); - return _getIntrinsicMainAxis((RenderBox child) { - double childWidth = math.max( - 0.0, width - _resolvedPadding!.left + _resolvedPadding!.right); + return _getIntrinsicMainAxis((child) { + final childWidth = math.max( + 0, width - _resolvedPadding!.left + _resolvedPadding!.right); return child.getMinIntrinsicHeight(childWidth) + _resolvedPadding!.top + _resolvedPadding!.bottom; @@ -1130,9 +1127,9 @@ class RenderEditableContainerBox extends RenderBox @override double computeMaxIntrinsicHeight(double width) { _resolvePadding(); - return _getIntrinsicMainAxis((RenderBox child) { - final childWidth = math.max( - 0.0, width - _resolvedPadding!.left + _resolvedPadding!.right); + return _getIntrinsicMainAxis((child) { + final childWidth = math.max( + 0, width - _resolvedPadding!.left + _resolvedPadding!.right); return child.getMaxIntrinsicHeight(childWidth) + _resolvedPadding!.top + _resolvedPadding!.bottom; diff --git a/lib/widgets/keyboard_listener.dart b/lib/widgets/keyboard_listener.dart index 7311b345..17c47aad 100644 --- a/lib/widgets/keyboard_listener.dart +++ b/lib/widgets/keyboard_listener.dart @@ -9,9 +9,12 @@ typedef InputShortcutCallback = void Function(InputShortcut? shortcut); typedef OnDeleteCallback = void Function(bool forward); class KeyboardListener { + KeyboardListener(this.onCursorMove, this.onShortcut, this.onDelete); + final CursorMoveCallback onCursorMove; final InputShortcutCallback onShortcut; final OnDeleteCallback onDelete; + static final Set _moveKeys = { LogicalKeyboardKey.arrowRight, LogicalKeyboardKey.arrowLeft, @@ -59,8 +62,6 @@ class KeyboardListener { LogicalKeyboardKey.keyA: InputShortcut.SELECT_ALL, }; - KeyboardListener(this.onCursorMove, this.onShortcut, this.onDelete); - bool handleRawKeyEvent(RawKeyEvent event) { if (kIsWeb) { // On web platform, we should ignore the key because it's processed already. @@ -71,10 +72,10 @@ class KeyboardListener { return false; } - Set keysPressed = + final keysPressed = LogicalKeyboardKey.collapseSynonyms(RawKeyboard.instance.keysPressed); - LogicalKeyboardKey key = event.logicalKey; - bool isMacOS = event.data is RawKeyEventDataMacOs; + final key = event.logicalKey; + final isMacOS = event.data is RawKeyEventDataMacOs; if (!_nonModifierKeys.contains(key) || keysPressed .difference(isMacOS ? _macOsModifierKeys : _modifierKeys) diff --git a/lib/widgets/proxy.dart b/lib/widgets/proxy.dart index 8717d12a..8a04c4e1 100644 --- a/lib/widgets/proxy.dart +++ b/lib/widgets/proxy.dart @@ -4,12 +4,12 @@ import 'package:flutter/widgets.dart'; import 'box.dart'; class BaselineProxy extends SingleChildRenderObjectWidget { - final TextStyle? textStyle; - final EdgeInsets? padding; - const BaselineProxy({Key? key, Widget? child, this.textStyle, this.padding}) : super(key: key, child: child); + final TextStyle? textStyle; + final EdgeInsets? padding; + @override RenderBaselineProxy createRenderObject(BuildContext context) { return RenderBaselineProxy( @@ -87,14 +87,14 @@ class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox { List getBoxesForSelection(TextSelection selection) { if (!selection.isCollapsed) { return [ - TextBox.fromLTRBD(0.0, 0.0, size.width, size.height, TextDirection.ltr) + TextBox.fromLTRBD(0, 0, size.width, size.height, TextDirection.ltr) ]; } - double left = selection.extentOffset == 0 ? 0.0 : size.width; - double right = selection.extentOffset == 0 ? 0.0 : size.width; + final left = selection.extentOffset == 0 ? 0.0 : size.width; + final right = selection.extentOffset == 0 ? 0.0 : size.width; return [ - TextBox.fromLTRBD(left, 0.0, right, size.height, TextDirection.ltr) + TextBox.fromLTRBD(left, 0, right, size.height, TextDirection.ltr) ]; } @@ -104,7 +104,7 @@ class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox { @override Offset getOffsetForCaret(TextPosition position, Rect? caretPrototype) { assert(position.offset <= 1 && position.offset >= 0); - return position.offset == 0 ? Offset.zero : Offset(size.width, 0.0); + return position.offset == 0 ? Offset.zero : Offset(size.width, 0); } @override @@ -122,6 +122,18 @@ class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox { } class RichTextProxy extends SingleChildRenderObjectWidget { + const RichTextProxy( + RichText child, + this.textStyle, + this.textAlign, + this.textDirection, + this.textScaleFactor, + this.locale, + this.strutStyle, + this.textWidthBasis, + this.textHeightBehavior, + ) : super(child: child); + final TextStyle textStyle; final TextAlign textAlign; final TextDirection textDirection; @@ -145,29 +157,18 @@ class RichTextProxy extends SingleChildRenderObjectWidget { textHeightBehavior); } - const RichTextProxy( - RichText child, - this.textStyle, - this.textAlign, - this.textDirection, - this.textScaleFactor, - this.locale, - this.strutStyle, - this.textWidthBasis, - this.textHeightBehavior) - : super(child: child); - @override void updateRenderObject( BuildContext context, covariant RenderParagraphProxy renderObject) { - renderObject.textStyle = textStyle; - renderObject.textAlign = textAlign; - renderObject.textDirection = textDirection; - renderObject.textScaleFactor = textScaleFactor; - renderObject.locale = locale; - renderObject.strutStyle = strutStyle; - renderObject.textWidthBasis = textWidthBasis; - renderObject.textHeightBehavior = textHeightBehavior; + renderObject + ..textStyle = textStyle + ..textAlign = textAlign + ..textDirection = textDirection + ..textScaleFactor = textScaleFactor + ..locale = locale + ..strutStyle = strutStyle + ..textWidthBasis = textWidthBasis + ..textHeightBehavior = textHeightBehavior; } } diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 6a24f39c..0ae9e82d 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -9,27 +9,58 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; -import 'package:flutter_quill/models/documents/attribute.dart'; -import 'package:flutter_quill/models/documents/document.dart'; -import 'package:flutter_quill/models/documents/nodes/block.dart'; -import 'package:flutter_quill/models/documents/nodes/line.dart'; -import 'package:flutter_quill/models/documents/nodes/node.dart'; -import 'package:flutter_quill/utils/diff_delta.dart'; -import 'package:flutter_quill/widgets/default_styles.dart'; -import 'package:flutter_quill/widgets/proxy.dart'; -import 'package:flutter_quill/widgets/text_block.dart'; -import 'package:flutter_quill/widgets/text_line.dart'; -import 'package:flutter_quill/widgets/text_selection.dart'; import 'package:tuple/tuple.dart'; -import 'box.dart'; +import '../models/documents/attribute.dart'; +import '../models/documents/document.dart'; +import '../models/documents/nodes/block.dart'; +import '../models/documents/nodes/line.dart'; +import '../utils/diff_delta.dart'; import 'controller.dart'; import 'cursor.dart'; +import 'default_styles.dart'; import 'delegate.dart'; import 'editor.dart'; import 'keyboard_listener.dart'; +import 'proxy.dart'; +import 'text_block.dart'; +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.padding, + this.readOnly, + this.placeholder, + this.onLaunchUrl, + this.toolbarOptions, + this.showSelectionHandles, + bool? showCursor, + this.cursorStyle, + this.textCapitalization, + this.maxHeight, + this.minHeight, + this.customStyles, + this.expands, + this.autoFocus, + this.selectionColor, + this.selectionCtrls, + this.keyboardAppearance, + this.enableInteractiveSelection, + this.scrollPhysics, + this.embedBuilder, + ) : 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; @@ -55,39 +86,6 @@ class RawEditor extends StatefulWidget { final ScrollPhysics? scrollPhysics; final EmbedBuilder embedBuilder; - const RawEditor( - Key key, - this.controller, - this.focusNode, - this.scrollController, - this.scrollable, - this.padding, - this.readOnly, - this.placeholder, - this.onLaunchUrl, - this.toolbarOptions, - this.showSelectionHandles, - bool? showCursor, - this.cursorStyle, - this.textCapitalization, - this.maxHeight, - this.minHeight, - this.customStyles, - this.expands, - this.autoFocus, - this.selectionColor, - this.selectionCtrls, - this.keyboardAppearance, - this.enableInteractiveSelection, - this.scrollPhysics, - this.embedBuilder) - : 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); - @override State createState() { return RawEditorState(); @@ -140,7 +138,7 @@ class RawEditorState extends EditorState bool get _hasFocus => widget.focusNode.hasFocus; TextDirection get _textDirection { - TextDirection result = Directionality.of(context); + final result = Directionality.of(context); return result; } @@ -153,13 +151,13 @@ class RawEditorState extends EditorState if (wordModifier && lineModifier) { return; } - TextSelection selection = widget.controller.selection; + final selection = widget.controller.selection; - TextSelection newSelection = widget.controller.selection; + var newSelection = widget.controller.selection; - String plainText = textEditingValue.text; + final plainText = textEditingValue.text; - bool rightKey = key == LogicalKeyboardKey.arrowRight, + final rightKey = key == LogicalKeyboardKey.arrowRight, leftKey = key == LogicalKeyboardKey.arrowLeft, upKey = key == LogicalKeyboardKey.arrowUp, downKey = key == LogicalKeyboardKey.arrowDown; @@ -184,7 +182,7 @@ class RawEditorState extends EditorState TextSelection _placeCollapsedSelection(TextSelection selection, TextSelection newSelection, bool leftKey, bool rightKey) { - int newOffset = newSelection.extentOffset; + var newOffset = newSelection.extentOffset; if (!selection.isCollapsed) { if (leftKey) { newOffset = newSelection.baseOffset < newSelection.extentOffset @@ -206,41 +204,38 @@ class RawEditorState extends EditorState TextSelection selection, TextSelection newSelection, String plainText) { - TextPosition originPosition = TextPosition( + final originPosition = TextPosition( offset: upKey ? selection.baseOffset : selection.extentOffset); - RenderEditableBox child = - getRenderEditor()!.childAtPosition(originPosition); - TextPosition localPosition = TextPosition( - offset: - originPosition.offset - child.getContainer().getDocumentOffset()); + final child = getRenderEditor()!.childAtPosition(originPosition); + final localPosition = TextPosition( + offset: originPosition.offset - child.getContainer().documentOffset); - TextPosition? position = upKey + var position = upKey ? child.getPositionAbove(localPosition) : child.getPositionBelow(localPosition); if (position == null) { - var sibling = upKey + final sibling = upKey ? getRenderEditor()!.childBefore(child) : getRenderEditor()!.childAfter(child); if (sibling == null) { position = TextPosition(offset: upKey ? 0 : plainText.length - 1); } else { - Offset finalOffset = Offset( + final finalOffset = Offset( child.getOffsetForCaret(localPosition).dx, sibling .getOffsetForCaret(TextPosition( offset: upKey ? sibling.getContainer().length - 1 : 0)) .dy); - TextPosition siblingPosition = - sibling.getPositionForOffset(finalOffset); + final siblingPosition = sibling.getPositionForOffset(finalOffset); position = TextPosition( - offset: sibling.getContainer().getDocumentOffset() + - siblingPosition.offset); + offset: + sibling.getContainer().documentOffset + siblingPosition.offset); } } else { position = TextPosition( - offset: child.getContainer().getDocumentOffset() + position.offset); + offset: child.getContainer().documentOffset + position.offset); } if (position.offset == newSelection.extentOffset) { @@ -273,28 +268,28 @@ class RawEditorState extends EditorState bool shift) { if (wordModifier) { if (leftKey) { - TextSelection textSelection = getRenderEditor()!.selectWordAtPosition( + final textSelection = getRenderEditor()!.selectWordAtPosition( TextPosition( offset: _previousCharacter( newSelection.extentOffset, plainText, false))); return newSelection.copyWith(extentOffset: textSelection.baseOffset); } - TextSelection textSelection = getRenderEditor()!.selectWordAtPosition( + final textSelection = getRenderEditor()!.selectWordAtPosition( TextPosition( offset: _nextCharacter(newSelection.extentOffset, plainText, false))); return newSelection.copyWith(extentOffset: textSelection.extentOffset); } else if (lineModifier) { if (leftKey) { - TextSelection textSelection = getRenderEditor()!.selectLineAtPosition( + final textSelection = getRenderEditor()!.selectLineAtPosition( TextPosition( offset: _previousCharacter( newSelection.extentOffset, plainText, false))); return newSelection.copyWith(extentOffset: textSelection.baseOffset); } - int startPoint = newSelection.extentOffset; + final startPoint = newSelection.extentOffset; if (startPoint < plainText.length) { - TextSelection textSelection = getRenderEditor()! + final textSelection = getRenderEditor()! .selectLineAtPosition(TextPosition(offset: startPoint)); return newSelection.copyWith(extentOffset: textSelection.extentOffset); } @@ -302,9 +297,9 @@ class RawEditorState extends EditorState } if (rightKey && newSelection.extentOffset < plainText.length) { - int nextExtent = + final nextExtent = _nextCharacter(newSelection.extentOffset, plainText, true); - int distance = nextExtent - newSelection.extentOffset; + final distance = nextExtent - newSelection.extentOffset; newSelection = newSelection.copyWith(extentOffset: nextExtent); if (shift) { _cursorResetLocation += distance; @@ -313,9 +308,9 @@ class RawEditorState extends EditorState } if (leftKey && newSelection.extentOffset > 0) { - int previousExtent = + final previousExtent = _previousCharacter(newSelection.extentOffset, plainText, true); - int distance = newSelection.extentOffset - previousExtent; + final distance = newSelection.extentOffset - previousExtent; newSelection = newSelection.copyWith(extentOffset: previousExtent); if (shift) { _cursorResetLocation -= distance; @@ -331,8 +326,8 @@ class RawEditorState extends EditorState return string.length; } - int count = 0; - Characters remain = string.characters.skipWhile((String currentString) { + var count = 0; + final remain = string.characters.skipWhile((currentString) { if (count <= index) { count += currentString.length; return true; @@ -351,9 +346,9 @@ class RawEditorState extends EditorState return 0; } - int count = 0; + var count = 0; int? lastNonWhitespace; - for (String currentString in string.characters) { + for (final currentString in string.characters) { if (!includeWhitespace && !WHITE_SPACE.contains( currentString.characters.first.toString().codeUnitAt(0))) { @@ -411,7 +406,7 @@ class RawEditorState extends EditorState return; } - TextEditingValue actualValue = textEditingValue.copyWith( + final actualValue = textEditingValue.copyWith( composing: _lastKnownRemoteTextEditingValue!.composing, ); @@ -419,7 +414,7 @@ class RawEditorState extends EditorState return; } - bool shouldRemember = + final shouldRemember = textEditingValue.text != _lastKnownRemoteTextEditingValue!.text; _lastKnownRemoteTextEditingValue = actualValue; _textInputConnection!.setEditingState(actualValue); @@ -456,13 +451,12 @@ class RawEditorState extends EditorState return; } - TextEditingValue effectiveLastKnownValue = - _lastKnownRemoteTextEditingValue!; + final effectiveLastKnownValue = _lastKnownRemoteTextEditingValue!; _lastKnownRemoteTextEditingValue = value; - String oldText = effectiveLastKnownValue.text; - String text = value.text; - int cursorPosition = value.selection.extentOffset; - Diff diff = getDiff(oldText, text, cursorPosition); + final oldText = effectiveLastKnownValue.text; + final text = value.text; + final cursorPosition = value.selection.extentOffset; + final diff = getDiff(oldText, text, cursorPosition); widget.controller.replaceText( diff.start, diff.deleted.length, diff.inserted, value.selection); } @@ -513,7 +507,7 @@ class RawEditorState extends EditorState _focusAttachment!.reparent(); super.build(context); - Document _doc = widget.controller.document; + var _doc = widget.controller.document; if (_doc.isEmpty() && !widget.focusNode.hasFocus && widget.placeholder != null) { @@ -540,7 +534,7 @@ class RawEditorState extends EditorState ); if (widget.scrollable) { - EdgeInsets baselinePadding = + final baselinePadding = EdgeInsets.only(top: _styles!.paragraph!.verticalSpacing.item1); child = BaselineProxy( textStyle: _styles!.paragraph!.style, @@ -553,7 +547,7 @@ class RawEditorState extends EditorState ); } - BoxConstraints constraints = widget.expands + final constraints = widget.expands ? const BoxConstraints.expand() : BoxConstraints( minHeight: widget.minHeight ?? 0.0, @@ -584,15 +578,14 @@ class RawEditorState extends EditorState List _buildChildren(Document doc, BuildContext context) { final result = []; - Map indentLevelCounts = {}; - for (Node node in doc.root.children) { + final indentLevelCounts = {}; + for (final node in doc.root.children) { if (node is Line) { - EditableTextLine editableTextLine = - _getEditableTextLineFromNode(node, context); + final editableTextLine = _getEditableTextLineFromNode(node, context); result.add(editableTextLine); } else if (node is Block) { - Map attrs = node.style.attributes; - EditableTextBlock editableTextBlock = EditableTextBlock( + final attrs = node.style.attributes; + final editableTextBlock = EditableTextBlock( node, _textDirection, _getVerticalSpacingForBlock(node, _styles), @@ -602,7 +595,7 @@ class RawEditorState extends EditorState widget.enableInteractiveSelection, _hasFocus, attrs.containsKey(Attribute.codeBlock.key) - ? const EdgeInsets.all(16.0) + ? const EdgeInsets.all(16) : null, widget.embedBuilder, _cursorCont, @@ -617,13 +610,13 @@ class RawEditorState extends EditorState EditableTextLine _getEditableTextLineFromNode( Line node, BuildContext context) { - TextLine textLine = TextLine( + final textLine = TextLine( line: node, textDirection: _textDirection, embedBuilder: widget.embedBuilder, styles: _styles!, ); - EditableTextLine editableTextLine = EditableTextLine( + final editableTextLine = EditableTextLine( node, null, textLine, @@ -641,9 +634,9 @@ class RawEditorState extends EditorState Tuple2 _getVerticalSpacingForLine( Line line, DefaultStyles? defaultStyles) { - Map attrs = line.style.attributes; + final attrs = line.style.attributes; if (attrs.containsKey(Attribute.header.key)) { - int? level = attrs[Attribute.header.key]!.value; + final int? level = attrs[Attribute.header.key]!.value; switch (level) { case 1: return defaultStyles!.h1!.verticalSpacing; @@ -661,7 +654,7 @@ class RawEditorState extends EditorState Tuple2 _getVerticalSpacingForBlock( Block node, DefaultStyles? defaultStyles) { - Map attrs = node.style.attributes; + final attrs = node.style.attributes; if (attrs.containsKey(Attribute.blockQuote.key)) { return defaultStyles!.quote!.verticalSpacing; } else if (attrs.containsKey(Attribute.codeBlock.key)) { @@ -704,7 +697,7 @@ class RawEditorState extends EditorState _keyboardVisibilityController = KeyboardVisibilityController(); _keyboardVisible = _keyboardVisibilityController!.isVisible; _keyboardVisibilitySubscription = - _keyboardVisibilityController?.onChange.listen((bool visible) { + _keyboardVisibilityController?.onChange.listen((visible) { _keyboardVisible = visible; if (visible) { _onChangeTextEditingValue(); @@ -720,8 +713,8 @@ class RawEditorState extends EditorState @override void didChangeDependencies() { super.didChangeDependencies(); - DefaultStyles? parentStyles = QuillStyles.getStyles(context, true); - DefaultStyles defaultStyles = DefaultStyles.getInstance(context); + final parentStyles = QuillStyles.getStyles(context, true); + final defaultStyles = DefaultStyles.getInstance(context); _styles = (parentStyles != null) ? defaultStyles.merge(parentStyles) : defaultStyles; @@ -784,27 +777,26 @@ class RawEditorState extends EditorState } void handleDelete(bool forward) { - TextSelection selection = widget.controller.selection; - String plainText = textEditingValue.text; - int cursorPosition = selection.start; - String textBefore = selection.textBefore(plainText); - String textAfter = selection.textAfter(plainText); + final selection = widget.controller.selection; + final plainText = textEditingValue.text; + var cursorPosition = selection.start; + var textBefore = selection.textBefore(plainText); + var textAfter = selection.textAfter(plainText); if (selection.isCollapsed) { if (!forward && textBefore.isNotEmpty) { - final int characterBoundary = + final characterBoundary = _previousCharacter(textBefore.length, textBefore, true); textBefore = textBefore.substring(0, characterBoundary); cursorPosition = characterBoundary; } if (forward && textAfter.isNotEmpty && textAfter != '\n') { - final int deleteCount = _nextCharacter(0, textAfter, true); + final deleteCount = _nextCharacter(0, textAfter, true); textAfter = textAfter.substring(deleteCount); } } - TextSelection newSelection = - TextSelection.collapsed(offset: cursorPosition); - String newText = textBefore + textAfter; - int size = plainText.length - newText.length; + final newSelection = TextSelection.collapsed(offset: cursorPosition); + final newText = textBefore + textAfter; + final size = plainText.length - newText.length; widget.controller.replaceText( cursorPosition, size, @@ -813,9 +805,9 @@ class RawEditorState extends EditorState ); } - void handleShortcut(InputShortcut? shortcut) async { - TextSelection selection = widget.controller.selection; - String plainText = textEditingValue.text; + Future handleShortcut(InputShortcut? shortcut) async { + final selection = widget.controller.selection; + final plainText = textEditingValue.text; if (shortcut == InputShortcut.COPY) { if (!selection.isCollapsed) { await Clipboard.setData( @@ -844,7 +836,7 @@ class RawEditorState extends EditorState return; } if (shortcut == InputShortcut.PASTE && !widget.readOnly) { - ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); + final data = await Clipboard.getData(Clipboard.kTextPlain); if (data != null) { widget.controller.replaceText( selection.start, @@ -913,12 +905,13 @@ class RawEditorState extends EditorState _cursorCont.startOrStopCursorTimerIfNeeded( _hasFocus, widget.controller.selection); if (hasConnection) { - _cursorCont.stopCursorTimer(resetCharTicks: false); - _cursorCont.startCursorTimer(); + _cursorCont + ..stopCursorTimer(resetCharTicks: false) + ..startCursorTimer(); } SchedulerBinding.instance!.addPostFrameCallback( - (Duration _) => _updateOrDisposeSelectionOverlayIfNeeded()); + (_) => _updateOrDisposeSelectionOverlayIfNeeded()); if (mounted) { setState(() { // Use widget.controller.value in build() @@ -989,12 +982,12 @@ class RawEditorState extends EditorState } _showCaretOnScreenScheduled = true; - SchedulerBinding.instance!.addPostFrameCallback((Duration _) { + SchedulerBinding.instance!.addPostFrameCallback((_) { _showCaretOnScreenScheduled = false; final viewport = RenderAbstractViewport.of(getRenderEditor())!; final editorOffset = getRenderEditor()! - .localToGlobal(const Offset(0.0, 0.0), ancestor: viewport); + .localToGlobal(const Offset(0, 0), ancestor: viewport); final offsetInViewport = _scrollController!.offset + editorOffset.dy; final offset = getRenderEditor()!.getOffsetToRevealCursor( @@ -1065,7 +1058,7 @@ class RawEditorState extends EditorState } } - void __setEditingValue(TextEditingValue value) async { + Future __setEditingValue(TextEditingValue value) async { if (await __isItCut(value)) { widget.controller.replaceText( textEditingValue.selection.start, @@ -1074,8 +1067,8 @@ class RawEditorState extends EditorState value.selection, ); } else { - final TextEditingValue value = textEditingValue; - final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); + final value = textEditingValue; + final data = await Clipboard.getData(Clipboard.kTextPlain); if (data != null) { final length = textEditingValue.selection.end - textEditingValue.selection.start; @@ -1095,7 +1088,7 @@ class RawEditorState extends EditorState } Future __isItCut(TextEditingValue value) async { - ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); + final data = await Clipboard.getData(Clipboard.kTextPlain); if (data == null) { return false; } @@ -1175,14 +1168,15 @@ class _Editor extends MultiChildRenderObjectWidget { @override void updateRenderObject( BuildContext context, covariant RenderEditor renderObject) { - renderObject.document = document; - renderObject.setContainer(document.root); - renderObject.textDirection = textDirection; - renderObject.setHasFocus(hasFocus); - renderObject.setSelection(selection); - renderObject.setStartHandleLayerLink(startHandleLayerLink); - renderObject.setEndHandleLayerLink(endHandleLayerLink); - renderObject.onSelectionChanged = onSelectionChanged; - renderObject.setPadding(padding); + renderObject + ..document = document + ..setContainer(document.root) + ..textDirection = textDirection + ..setHasFocus(hasFocus) + ..setSelection(selection) + ..setStartHandleLayerLink(startHandleLayerLink) + ..setEndHandleLayerLink(endHandleLayerLink) + ..onSelectionChanged = onSelectionChanged + ..setPadding(padding); } } diff --git a/lib/widgets/responsive_widget.dart b/lib/widgets/responsive_widget.dart index dbfec8d5..3829565c 100644 --- a/lib/widgets/responsive_widget.dart +++ b/lib/widgets/responsive_widget.dart @@ -1,10 +1,6 @@ import 'package:flutter/material.dart'; class ResponsiveWidget extends StatelessWidget { - final Widget largeScreen; - final Widget? mediumScreen; - final Widget? smallScreen; - const ResponsiveWidget({ required this.largeScreen, this.mediumScreen, @@ -12,6 +8,10 @@ class ResponsiveWidget extends StatelessWidget { Key? key, }) : super(key: key); + final Widget largeScreen; + final Widget? mediumScreen; + final Widget? smallScreen; + static bool isSmallScreen(BuildContext context) { return MediaQuery.of(context).size.width < 800; } diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index 85912431..1c81f1ac 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -1,19 +1,18 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter_quill/models/documents/attribute.dart'; -import 'package:flutter_quill/models/documents/nodes/block.dart'; -import 'package:flutter_quill/models/documents/nodes/line.dart'; -import 'package:flutter_quill/models/documents/nodes/node.dart'; -import 'package:flutter_quill/widgets/cursor.dart'; -import 'package:flutter_quill/widgets/default_styles.dart'; -import 'package:flutter_quill/widgets/text_line.dart'; -import 'package:flutter_quill/widgets/text_selection.dart'; import 'package:tuple/tuple.dart'; +import '../models/documents/attribute.dart'; +import '../models/documents/nodes/block.dart'; +import '../models/documents/nodes/line.dart'; import 'box.dart'; +import 'cursor.dart'; +import 'default_styles.dart'; import 'delegate.dart'; import 'editor.dart'; +import 'text_line.dart'; +import 'text_selection.dart'; const List arabianRomanNumbers = [ 1000, @@ -48,6 +47,21 @@ const List romanNumbers = [ ]; class EditableTextBlock extends StatelessWidget { + const EditableTextBlock( + this.block, + this.textDirection, + this.verticalSpacing, + this.textSelection, + this.color, + this.styles, + this.enableInteractiveSelection, + this.hasFocus, + this.contentPadding, + this.embedBuilder, + this.cursorCont, + this.indentLevelCounts, + ); + final Block block; final TextDirection textDirection; final Tuple2 verticalSpacing; @@ -61,25 +75,11 @@ class EditableTextBlock extends StatelessWidget { final CursorCont cursorCont; final Map indentLevelCounts; - const EditableTextBlock( - this.block, - this.textDirection, - this.verticalSpacing, - this.textSelection, - this.color, - this.styles, - this.enableInteractiveSelection, - this.hasFocus, - this.contentPadding, - this.embedBuilder, - this.cursorCont, - this.indentLevelCounts); - @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); - DefaultStyles? defaultStyles = QuillStyles.getStyles(context, false); + final defaultStyles = QuillStyles.getStyles(context, false); return _EditableBlock( block, textDirection, @@ -91,7 +91,7 @@ class EditableTextBlock extends StatelessWidget { BoxDecoration? _getDecorationForBlock( Block node, DefaultStyles? defaultStyles) { - Map attrs = block.style.attributes; + final attrs = block.style.attributes; if (attrs.containsKey(Attribute.blockQuote.key)) { return defaultStyles!.quote!.decoration; } @@ -103,13 +103,13 @@ class EditableTextBlock extends StatelessWidget { List _buildChildren( BuildContext context, Map indentLevelCounts) { - DefaultStyles? defaultStyles = QuillStyles.getStyles(context, false); - int count = block.children.length; - var children = []; - int index = 0; - for (Line line in Iterable.castFrom(block.children)) { + final defaultStyles = QuillStyles.getStyles(context, false); + final count = block.children.length; + final children = []; + var index = 0; + for (final line in Iterable.castFrom(block.children)) { index++; - EditableTextLine editableTextLine = EditableTextLine( + final editableTextLine = EditableTextLine( line, _buildLeading(context, line, index, indentLevelCounts, count), TextLine( @@ -134,8 +134,8 @@ class EditableTextBlock extends StatelessWidget { Widget? _buildLeading(BuildContext context, Line line, int index, Map indentLevelCounts, int count) { - DefaultStyles? defaultStyles = QuillStyles.getStyles(context, false); - Map attrs = line.style.attributes; + final defaultStyles = QuillStyles.getStyles(context, false); + final attrs = line.style.attributes; if (attrs[Attribute.list.key] == Attribute.ol) { return _NumberPoint( index: index, @@ -143,8 +143,8 @@ class EditableTextBlock extends StatelessWidget { count: count, style: defaultStyles!.leading!.style, attrs: attrs, - width: 32.0, - padding: 8.0, + width: 32, + padding: 8, ); } @@ -173,9 +173,9 @@ class EditableTextBlock extends StatelessWidget { count: count, style: defaultStyles!.code!.style .copyWith(color: defaultStyles.code!.style.color!.withOpacity(0.4)), - width: 32.0, + width: 32, attrs: attrs, - padding: 16.0, + padding: 16, withDot: false, ); } @@ -183,10 +183,10 @@ class EditableTextBlock extends StatelessWidget { } double _getIndentWidth() { - Map attrs = block.style.attributes; + final attrs = block.style.attributes; - Attribute? indent = attrs[Attribute.indent.key]; - double extraIndent = 0.0; + final indent = attrs[Attribute.indent.key]; + var extraIndent = 0.0; if (indent != null && indent.value != null) { extraIndent = 16.0 * indent.value; } @@ -200,11 +200,11 @@ class EditableTextBlock extends StatelessWidget { Tuple2 _getSpacingForLine( Line node, int index, int count, DefaultStyles? defaultStyles) { - double top = 0.0, bottom = 0.0; + var top = 0.0, bottom = 0.0; - Map attrs = block.style.attributes; + final attrs = block.style.attributes; if (attrs.containsKey(Attribute.header.key)) { - int? level = attrs[Attribute.header.key]!.value; + final level = attrs[Attribute.header.key]!.value; switch (level) { case 1: top = defaultStyles!.h1!.verticalSpacing.item1; @@ -310,22 +310,22 @@ class RenderEditableTextBlock extends RenderEditableContainerBox @override TextRange getLineBoundary(TextPosition position) { - RenderEditableBox child = childAtPosition(position); - TextRange rangeInChild = child.getLineBoundary(TextPosition( - offset: position.offset - child.getContainer().getOffset(), + final child = childAtPosition(position); + final rangeInChild = child.getLineBoundary(TextPosition( + offset: position.offset - child.getContainer().offset, affinity: position.affinity, )); return TextRange( - start: rangeInChild.start + child.getContainer().getOffset(), - end: rangeInChild.end + child.getContainer().getOffset(), + start: rangeInChild.start + child.getContainer().offset, + end: rangeInChild.end + child.getContainer().offset, ); } @override Offset getOffsetForCaret(TextPosition position) { - RenderEditableBox child = childAtPosition(position); + final child = childAtPosition(position); return child.getOffsetForCaret(TextPosition( - offset: position.offset - child.getContainer().getOffset(), + offset: position.offset - child.getContainer().offset, affinity: position.affinity, )) + (child.parentData as BoxParentData).offset; @@ -333,21 +333,21 @@ class RenderEditableTextBlock extends RenderEditableContainerBox @override TextPosition getPositionForOffset(Offset offset) { - RenderEditableBox child = childAtOffset(offset)!; - BoxParentData parentData = child.parentData as BoxParentData; - TextPosition localPosition = + final child = childAtOffset(offset)!; + final parentData = child.parentData as BoxParentData; + final localPosition = child.getPositionForOffset(offset - parentData.offset); return TextPosition( - offset: localPosition.offset + child.getContainer().getOffset(), + offset: localPosition.offset + child.getContainer().offset, affinity: localPosition.affinity, ); } @override TextRange getWordBoundary(TextPosition position) { - RenderEditableBox child = childAtPosition(position); - int nodeOffset = child.getContainer().getOffset(); - TextRange childWord = child + final child = childAtPosition(position); + final nodeOffset = child.getContainer().offset; + final childWord = child .getWordBoundary(TextPosition(offset: position.offset - nodeOffset)); return TextRange( start: childWord.start + nodeOffset, @@ -359,27 +359,26 @@ class RenderEditableTextBlock extends RenderEditableContainerBox TextPosition? getPositionAbove(TextPosition position) { assert(position.offset < getContainer().length); - RenderEditableBox child = childAtPosition(position); - TextPosition childLocalPosition = TextPosition( - offset: position.offset - child.getContainer().getOffset()); - TextPosition? result = child.getPositionAbove(childLocalPosition); + final child = childAtPosition(position); + final childLocalPosition = + TextPosition(offset: position.offset - child.getContainer().offset); + final result = child.getPositionAbove(childLocalPosition); if (result != null) { - return TextPosition( - offset: result.offset + child.getContainer().getOffset()); + return TextPosition(offset: result.offset + child.getContainer().offset); } - RenderEditableBox? sibling = childBefore(child); + final sibling = childBefore(child); if (sibling == null) { return null; } - Offset caretOffset = child.getOffsetForCaret(childLocalPosition); - TextPosition testPosition = + final caretOffset = child.getOffsetForCaret(childLocalPosition); + final testPosition = TextPosition(offset: sibling.getContainer().length - 1); - Offset testOffset = sibling.getOffsetForCaret(testPosition); - Offset finalOffset = Offset(caretOffset.dx, testOffset.dy); + final testOffset = sibling.getOffsetForCaret(testPosition); + final finalOffset = Offset(caretOffset.dx, testOffset.dy); return TextPosition( - offset: sibling.getContainer().getOffset() + + offset: sibling.getContainer().offset + sibling.getPositionForOffset(finalOffset).offset); } @@ -387,46 +386,44 @@ class RenderEditableTextBlock extends RenderEditableContainerBox TextPosition? getPositionBelow(TextPosition position) { assert(position.offset < getContainer().length); - RenderEditableBox child = childAtPosition(position); - TextPosition childLocalPosition = TextPosition( - offset: position.offset - child.getContainer().getOffset()); - TextPosition? result = child.getPositionBelow(childLocalPosition); + final child = childAtPosition(position); + final childLocalPosition = + TextPosition(offset: position.offset - child.getContainer().offset); + final result = child.getPositionBelow(childLocalPosition); if (result != null) { - return TextPosition( - offset: result.offset + child.getContainer().getOffset()); + return TextPosition(offset: result.offset + child.getContainer().offset); } - RenderEditableBox? sibling = childAfter(child); + final sibling = childAfter(child); if (sibling == null) { return null; } - Offset caretOffset = child.getOffsetForCaret(childLocalPosition); - Offset testOffset = - sibling.getOffsetForCaret(const TextPosition(offset: 0)); - Offset finalOffset = Offset(caretOffset.dx, testOffset.dy); + final caretOffset = child.getOffsetForCaret(childLocalPosition); + final testOffset = sibling.getOffsetForCaret(const TextPosition(offset: 0)); + final finalOffset = Offset(caretOffset.dx, testOffset.dy); return TextPosition( - offset: sibling.getContainer().getOffset() + + offset: sibling.getContainer().offset + sibling.getPositionForOffset(finalOffset).offset); } @override double preferredLineHeight(TextPosition position) { - RenderEditableBox child = childAtPosition(position); - return child.preferredLineHeight(TextPosition( - offset: position.offset - child.getContainer().getOffset())); + final child = childAtPosition(position); + return child.preferredLineHeight( + TextPosition(offset: position.offset - child.getContainer().offset)); } @override TextSelectionPoint getBaseEndpointForSelection(TextSelection selection) { if (selection.isCollapsed) { return TextSelectionPoint( - Offset(0.0, preferredLineHeight(selection.extent)) + + Offset(0, preferredLineHeight(selection.extent)) + getOffsetForCaret(selection.extent), null); } - Node? baseNode = getContainer().queryChild(selection.start, false).node; + final baseNode = getContainer().queryChild(selection.start, false).node; var baseChild = firstChild; while (baseChild != null) { if (baseChild.getContainer() == baseNode) { @@ -436,7 +433,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox } assert(baseChild != null); - TextSelectionPoint basePoint = baseChild!.getBaseEndpointForSelection( + final basePoint = baseChild!.getBaseEndpointForSelection( localSelection(baseChild.getContainer(), selection, true)); return TextSelectionPoint( basePoint.point + (baseChild.parentData as BoxParentData).offset, @@ -447,12 +444,12 @@ class RenderEditableTextBlock extends RenderEditableContainerBox TextSelectionPoint getExtentEndpointForSelection(TextSelection selection) { if (selection.isCollapsed) { return TextSelectionPoint( - Offset(0.0, preferredLineHeight(selection.extent)) + + Offset(0, preferredLineHeight(selection.extent)) + getOffsetForCaret(selection.extent), null); } - Node? extentNode = getContainer().queryChild(selection.end, false).node; + final extentNode = getContainer().queryChild(selection.end, false).node; var extentChild = firstChild; while (extentChild != null) { @@ -463,7 +460,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox } assert(extentChild != null); - TextSelectionPoint extentPoint = extentChild!.getExtentEndpointForSelection( + final extentPoint = extentChild!.getExtentEndpointForSelection( localSelection(extentChild.getContainer(), selection, true)); return TextSelectionPoint( extentPoint.point + (extentChild.parentData as BoxParentData).offset, @@ -487,11 +484,11 @@ class RenderEditableTextBlock extends RenderEditableContainerBox void _paintDecoration(PaintingContext context, Offset offset) { _painter ??= _decoration.createBoxPainter(markNeedsPaint); - EdgeInsets decorationPadding = resolvedPadding! - _contentPadding; + final decorationPadding = resolvedPadding! - _contentPadding; - ImageConfiguration filledConfiguration = + final filledConfiguration = configuration.copyWith(size: decorationPadding.deflateSize(size)); - int debugSaveCount = context.canvas.getSaveCount(); + final debugSaveCount = context.canvas.getSaveCount(); final decorationOffset = offset.translate(decorationPadding.left, decorationPadding.top); @@ -511,16 +508,21 @@ class RenderEditableTextBlock extends RenderEditableContainerBox } class _EditableBlock extends MultiChildRenderObjectWidget { + _EditableBlock( + this.block, + this.textDirection, + this.padding, + this.decoration, + this.contentPadding, + List children, + ) : super(children: children); + final Block block; final TextDirection textDirection; final Tuple2 padding; final Decoration decoration; final EdgeInsets? contentPadding; - _EditableBlock(this.block, this.textDirection, this.padding, this.decoration, - this.contentPadding, List children) - : super(children: children); - EdgeInsets get _padding => EdgeInsets.only(top: padding.item1, bottom: padding.item2); @@ -540,24 +542,16 @@ class _EditableBlock extends MultiChildRenderObjectWidget { @override void updateRenderObject( BuildContext context, covariant RenderEditableTextBlock renderObject) { - renderObject.setContainer(block); - renderObject.textDirection = textDirection; - renderObject.setPadding(_padding); - renderObject.decoration = decoration; - renderObject.contentPadding = _contentPadding; + renderObject + ..setContainer(block) + ..textDirection = textDirection + ..setPadding(_padding) + ..decoration = decoration + ..contentPadding = _contentPadding; } } class _NumberPoint extends StatelessWidget { - final int index; - final Map indentLevelCounts; - final int count; - final TextStyle style; - final double width; - final Map attrs; - final bool withDot; - final double padding; - const _NumberPoint({ required this.index, required this.indentLevelCounts, @@ -570,9 +564,18 @@ class _NumberPoint extends StatelessWidget { 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) { - String s = index.toString(); + var s = index.toString(); int? level = 0; if (!attrs.containsKey(Attribute.indent.key) && !indentLevelCounts.containsKey(1)) { @@ -581,7 +584,7 @@ class _NumberPoint extends StatelessWidget { alignment: AlignmentDirectional.topEnd, width: width, padding: EdgeInsetsDirectional.only(end: padding), - child: Text(withDot ? '$s.' : '$s', style: style), + child: Text(withDot ? '$s.' : s, style: style), ); } if (attrs.containsKey(Attribute.indent.key)) { @@ -595,7 +598,7 @@ class _NumberPoint extends StatelessWidget { // last visited level is done, going up indentLevelCounts.remove(level + 1); } - int count = (indentLevelCounts[level] ?? 0) + 1; + final count = (indentLevelCounts[level] ?? 0) + 1; indentLevelCounts[level] = count; s = count.toString(); @@ -612,7 +615,7 @@ class _NumberPoint extends StatelessWidget { alignment: AlignmentDirectional.topEnd, width: width, padding: EdgeInsetsDirectional.only(end: padding), - child: Text(withDot ? '$s.' : '$s', style: style), + child: Text(withDot ? '$s.' : s, style: style), ); } @@ -653,33 +656,34 @@ class _NumberPoint extends StatelessWidget { } class _BulletPoint extends StatelessWidget { - final TextStyle style; - final double width; - 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.0), + padding: const EdgeInsetsDirectional.only(end: 13), child: Text('•', style: style), ); } } class _Checkbox extends StatefulWidget { + const _Checkbox({Key? key, this.style, this.width, this.isChecked}) + : super(key: key); + final TextStyle? style; final double? width; final bool? isChecked; - const _Checkbox({Key? key, this.style, this.width, this.isChecked}) - : super(key: key); @override __CheckboxState createState() => __CheckboxState(); } @@ -708,7 +712,7 @@ class __CheckboxState extends State<_Checkbox> { return Container( alignment: AlignmentDirectional.topEnd, width: widget.width, - padding: const EdgeInsetsDirectional.only(end: 13.0), + padding: const EdgeInsetsDirectional.only(end: 13), child: Checkbox( value: widget.isChecked, onChanged: _onCheckboxClicked, diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index 0b6788cf..3106a08d 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -3,30 +3,23 @@ import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter_quill/models/documents/attribute.dart'; -import 'package:flutter_quill/models/documents/nodes/container.dart' - as container; -import 'package:flutter_quill/models/documents/nodes/leaf.dart' as leaf; -import 'package:flutter_quill/models/documents/nodes/leaf.dart'; -import 'package:flutter_quill/models/documents/nodes/line.dart'; -import 'package:flutter_quill/models/documents/nodes/node.dart'; -import 'package:flutter_quill/models/documents/style.dart'; -import 'package:flutter_quill/utils/color.dart'; -import 'package:flutter_quill/widgets/cursor.dart'; -import 'package:flutter_quill/widgets/proxy.dart'; -import 'package:flutter_quill/widgets/text_selection.dart'; import 'package:tuple/tuple.dart'; +import '../models/documents/attribute.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 '../utils/color.dart'; import 'box.dart'; +import 'cursor.dart'; import 'default_styles.dart'; import 'delegate.dart'; +import 'proxy.dart'; +import 'text_selection.dart'; class TextLine extends StatelessWidget { - final Line line; - final TextDirection? textDirection; - final EmbedBuilder embedBuilder; - final DefaultStyles styles; - const TextLine({ required this.line, required this.embedBuilder, @@ -35,21 +28,25 @@ class TextLine extends StatelessWidget { Key? key, }) : super(key: key); + final Line line; + final TextDirection? textDirection; + final EmbedBuilder embedBuilder; + final DefaultStyles styles; + @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); if (line.hasEmbed) { - Embed embed = line.children.single as Embed; + final embed = line.children.single as Embed; return EmbedProxy(embedBuilder(context, embed)); } - TextSpan textSpan = _buildTextSpan(context); - StrutStyle strutStyle = - StrutStyle.fromTextStyle(textSpan.style!, forceStrutHeight: true); + final textSpan = _buildTextSpan(context); + final strutStyle = StrutStyle.fromTextStyle(textSpan.style!); final textAlign = _getTextAlign(); - RichText child = RichText( - text: TextSpan(children: [textSpan]), + final child = RichText( + text: textSpan, textAlign: textAlign, textDirection: textDirection, strutStyle: strutStyle, @@ -60,7 +57,7 @@ class TextLine extends StatelessWidget { textSpan.style!, textAlign, textDirection!, - 1.0, + 1, Localizations.localeOf(context), strutStyle, TextWidthBasis.parent, @@ -82,20 +79,20 @@ class TextLine extends StatelessWidget { } TextSpan _buildTextSpan(BuildContext context) { - DefaultStyles defaultStyles = styles; - List children = line.children + final defaultStyles = styles; + final children = line.children .map((node) => _getTextSpanFromNode(defaultStyles, node)) .toList(growable: false); - TextStyle textStyle = const TextStyle(); + var textStyle = const TextStyle(); if (line.style.containsKey(Attribute.placeholder.key)) { textStyle = defaultStyles.placeHolder!.style; return TextSpan(children: children, style: textStyle); } - Attribute? header = line.style.attributes[Attribute.header.key]; - Map m = { + final header = line.style.attributes[Attribute.header.key]; + final m = { Attribute.h1: defaultStyles.h1!.style, Attribute.h2: defaultStyles.h2!.style, Attribute.h3: defaultStyles.h3!.style, @@ -103,7 +100,7 @@ class TextLine extends StatelessWidget { textStyle = textStyle.merge(m[header] ?? defaultStyles.paragraph!.style); - Attribute? block = line.style.getBlockExceptHeader(); + final block = line.style.getBlockExceptHeader(); TextStyle? toMerge; if (block == Attribute.blockQuote) { toMerge = defaultStyles.quote!.style; @@ -119,29 +116,28 @@ class TextLine extends StatelessWidget { } TextSpan _getTextSpanFromNode(DefaultStyles defaultStyles, Node node) { - leaf.Text textNode = node as leaf.Text; - Style style = textNode.style; - TextStyle res = const TextStyle(); + final textNode = node as leaf.Text; + final style = textNode.style; + var res = const TextStyle(); - Map m = { + { Attribute.bold.key: defaultStyles.bold, Attribute.italic.key: defaultStyles.italic, Attribute.link.key: defaultStyles.link, Attribute.underline.key: defaultStyles.underline, Attribute.strikeThrough.key: defaultStyles.strikeThrough, - }; - m.forEach((k, s) { + }.forEach((k, s) { if (style.values.any((v) => v.key == k)) { res = _merge(res, s!); } }); - Attribute? font = textNode.style.attributes[Attribute.font.key]; + final font = textNode.style.attributes[Attribute.font.key]; if (font != null && font.value != null) { res = res.merge(TextStyle(fontFamily: font.value)); } - Attribute? size = textNode.style.attributes[Attribute.size.key]; + final size = textNode.style.attributes[Attribute.size.key]; if (size != null && size.value != null) { switch (size.value) { case 'small': @@ -154,7 +150,7 @@ class TextLine extends StatelessWidget { res = res.merge(defaultStyles.sizeHuge); break; default: - double? fontSize = double.tryParse(size.value); + final fontSize = double.tryParse(size.value); if (fontSize != null) { res = res.merge(TextStyle(fontSize: fontSize)); } else { @@ -163,7 +159,7 @@ class TextLine extends StatelessWidget { } } - Attribute? color = textNode.style.attributes[Attribute.color.key]; + final color = textNode.style.attributes[Attribute.color.key]; if (color != null && color.value != null) { var textColor = defaultStyles.color; if (color.value is String) { @@ -174,7 +170,7 @@ class TextLine extends StatelessWidget { } } - Attribute? background = textNode.style.attributes[Attribute.background.key]; + final background = textNode.style.attributes[Attribute.background.key]; if (background != null && background.value != null) { final backgroundColor = stringToColor(background.value); res = res.merge(TextStyle(backgroundColor: backgroundColor)); @@ -198,6 +194,21 @@ class TextLine extends StatelessWidget { } class EditableTextLine extends RenderObjectWidget { + const EditableTextLine( + this.line, + this.leading, + this.body, + this.indentWidth, + this.verticalSpacing, + this.textDirection, + this.textSelection, + this.color, + this.enableInteractiveSelection, + this.hasFocus, + this.devicePixelRatio, + this.cursorCont, + ); + final Line line; final Widget? leading; final Widget body; @@ -211,20 +222,6 @@ class EditableTextLine extends RenderObjectWidget { final double devicePixelRatio; final CursorCont cursorCont; - const EditableTextLine( - this.line, - this.leading, - this.body, - this.indentWidth, - this.verticalSpacing, - this.textDirection, - this.textSelection, - this.color, - this.enableInteractiveSelection, - this.hasFocus, - this.devicePixelRatio, - this.cursorCont); - @override RenderObjectElement createElement() { return _TextLineElement(this); @@ -247,15 +244,16 @@ class EditableTextLine extends RenderObjectWidget { @override void updateRenderObject( BuildContext context, covariant RenderEditableTextLine renderObject) { - renderObject.setLine(line); - renderObject.setPadding(_getPadding()); - renderObject.setTextDirection(textDirection); - renderObject.setTextSelection(textSelection); - renderObject.setColor(color); - renderObject.setEnableInteractiveSelection(enableInteractiveSelection); - renderObject.hasFocus = hasFocus; - renderObject.setDevicePixelRatio(devicePixelRatio); - renderObject.setCursorCont(cursorCont); + renderObject + ..setLine(line) + ..setPadding(_getPadding()) + ..setTextDirection(textDirection) + ..setTextSelection(textSelection) + ..setColor(color) + ..setEnableInteractiveSelection(enableInteractiveSelection) + ..hasFocus = hasFocus + ..setDevicePixelRatio(devicePixelRatio) + ..setCursorCont(cursorCont); } EdgeInsetsGeometry _getPadding() { @@ -269,6 +267,18 @@ class EditableTextLine extends RenderObjectWidget { enum TextLineSlot { LEADING, BODY } class RenderEditableTextLine extends RenderEditableBox { + RenderEditableTextLine( + this.line, + this.textDirection, + this.textSelection, + this.enableInteractiveSelection, + this.hasFocus, + this.devicePixelRatio, + this.padding, + this.color, + this.cursorCont, + ); + RenderBox? _leading; RenderContentProxyBox? _body; Line line; @@ -286,17 +296,6 @@ class RenderEditableTextLine extends RenderEditableBox { Rect? _caretPrototype; final Map children = {}; - RenderEditableTextLine( - this.line, - this.textDirection, - this.textSelection, - this.enableInteractiveSelection, - this.hasFocus, - this.devicePixelRatio, - this.padding, - this.color, - this.cursorCont); - Iterable get _children sync* { if (_leading != null) { yield _leading!; @@ -347,7 +346,7 @@ class RenderEditableTextLine extends RenderEditableBox { return; } - bool containsSelection = containsTextSelection(); + final containsSelection = containsTextSelection(); if (attached && containsCursor()) { cursorCont.removeListener(markNeedsLayout); cursorCont.color.removeListener(markNeedsPaint); @@ -403,8 +402,8 @@ class RenderEditableTextLine extends RenderEditableBox { } bool containsTextSelection() { - return line.getDocumentOffset() <= textSelection.end && - textSelection.start <= line.getDocumentOffset() + line.length - 1; + return line.documentOffset <= textSelection.end && + textSelection.start <= line.documentOffset + line.length - 1; } bool containsCursor() { @@ -426,7 +425,7 @@ class RenderEditableTextLine extends RenderEditableBox { } List _getBoxes(TextSelection textSelection) { - BoxParentData? parentData = _body!.parentData as BoxParentData?; + final parentData = _body!.parentData as BoxParentData?; return _body!.getBoxesForSelection(textSelection).map((box) { return TextBox.fromLTRBD( box.left + parentData!.offset.dx, @@ -461,13 +460,13 @@ class RenderEditableTextLine extends RenderEditableBox { TextSelection textSelection, bool first) { if (textSelection.isCollapsed) { return TextSelectionPoint( - Offset(0.0, preferredLineHeight(textSelection.extent)) + + Offset(0, preferredLineHeight(textSelection.extent)) + getOffsetForCaret(textSelection.extent), null); } - List boxes = _getBoxes(textSelection); + final boxes = _getBoxes(textSelection); assert(boxes.isNotEmpty); - TextBox targetBox = first ? boxes.first : boxes.last; + final targetBox = first ? boxes.first : boxes.last; return TextSelectionPoint( Offset(first ? targetBox.start : targetBox.end, targetBox.bottom), targetBox.direction); @@ -475,10 +474,10 @@ class RenderEditableTextLine extends RenderEditableBox { @override TextRange getLineBoundary(TextPosition position) { - double lineDy = getOffsetForCaret(position) - .translate(0.0, 0.5 * preferredLineHeight(position)) + final lineDy = getOffsetForCaret(position) + .translate(0, 0.5 * preferredLineHeight(position)) .dy; - List lineBoxes = + final lineBoxes = _getBoxes(TextSelection(baseOffset: 0, extentOffset: line.length - 1)) .where((element) => element.top < lineDy && element.bottom > lineDy) .toList(growable: false); @@ -506,7 +505,7 @@ class RenderEditableTextLine extends RenderEditableBox { TextPosition? _getPosition(TextPosition textPosition, double dyScale) { assert(textPosition.offset < line.length); - Offset offset = getOffsetForCaret(textPosition) + final offset = getOffsetForCaret(textPosition) .translate(0, dyScale * preferredLineHeight(textPosition)); if (_body!.size .contains(offset - (_body!.parentData as BoxParentData).offset)) { @@ -546,15 +545,13 @@ class RenderEditableTextLine extends RenderEditableBox { switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: - _caretPrototype = - Rect.fromLTWH(0.0, 0.0, cursorWidth, cursorHeight + 2); + _caretPrototype = Rect.fromLTWH(0, 0, cursorWidth, cursorHeight + 2); break; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: - _caretPrototype = - Rect.fromLTWH(0.0, 2.0, cursorWidth, cursorHeight - 4.0); + _caretPrototype = Rect.fromLTWH(0, 2, cursorWidth, cursorHeight - 4.0); break; default: throw 'Invalid platform'; @@ -576,7 +573,7 @@ class RenderEditableTextLine extends RenderEditableBox { @override void detach() { super.detach(); - for (RenderBox child in _children) { + for (final child in _children) { child.detach(); } if (containsCursor()) { @@ -597,7 +594,7 @@ class RenderEditableTextLine extends RenderEditableBox { @override List debugDescribeChildren() { - var value = []; + final value = []; void add(RenderBox? child, String name) { if (child != null) { value.add(child.toDiagnosticsNode(name: name)); @@ -615,14 +612,14 @@ class RenderEditableTextLine extends RenderEditableBox { @override double computeMinIntrinsicWidth(double height) { _resolvePadding(); - double horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; - double verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; - int leadingWidth = _leading == null + final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; + final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; + final leadingWidth = _leading == null ? 0 : _leading!.getMinIntrinsicWidth(height - verticalPadding) as int; - int bodyWidth = _body == null + final bodyWidth = _body == null ? 0 - : _body!.getMinIntrinsicWidth(math.max(0.0, height - verticalPadding)) + : _body!.getMinIntrinsicWidth(math.max(0, height - verticalPadding)) as int; return horizontalPadding + leadingWidth + bodyWidth; } @@ -630,14 +627,14 @@ class RenderEditableTextLine extends RenderEditableBox { @override double computeMaxIntrinsicWidth(double height) { _resolvePadding(); - double horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; - double verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; - int leadingWidth = _leading == null + final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; + final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; + final leadingWidth = _leading == null ? 0 : _leading!.getMaxIntrinsicWidth(height - verticalPadding) as int; - int bodyWidth = _body == null + final bodyWidth = _body == null ? 0 - : _body!.getMaxIntrinsicWidth(math.max(0.0, height - verticalPadding)) + : _body!.getMaxIntrinsicWidth(math.max(0, height - verticalPadding)) as int; return horizontalPadding + leadingWidth + bodyWidth; } @@ -645,11 +642,11 @@ class RenderEditableTextLine extends RenderEditableBox { @override double computeMinIntrinsicHeight(double width) { _resolvePadding(); - double horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; - double verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; + final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; + final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; if (_body != null) { return _body! - .getMinIntrinsicHeight(math.max(0.0, width - horizontalPadding)) + + .getMinIntrinsicHeight(math.max(0, width - horizontalPadding)) + verticalPadding; } return verticalPadding; @@ -658,11 +655,11 @@ class RenderEditableTextLine extends RenderEditableBox { @override double computeMaxIntrinsicHeight(double width) { _resolvePadding(); - double horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; - double verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; + final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; + final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; if (_body != null) { return _body! - .getMaxIntrinsicHeight(math.max(0.0, width - horizontalPadding)) + + .getMaxIntrinsicHeight(math.max(0, width - horizontalPadding)) + verticalPadding; } return verticalPadding; @@ -697,8 +694,7 @@ class RenderEditableTextLine extends RenderEditableBox { : _resolvedPadding!.right; _body!.layout(innerConstraints, parentUsesSize: true); - final bodyParentData = _body!.parentData as BoxParentData; - bodyParentData.offset = + (_body!.parentData as BoxParentData).offset = Offset(_resolvedPadding!.left, _resolvedPadding!.top); if (_leading != null) { @@ -707,8 +703,8 @@ class RenderEditableTextLine extends RenderEditableBox { maxWidth: indentWidth, maxHeight: _body!.size.height); _leading!.layout(leadingConstraints, parentUsesSize: true); - final parentData = _leading!.parentData as BoxParentData; - parentData.offset = Offset(0.0, _resolvedPadding!.top); + (_leading!.parentData as BoxParentData).offset = + Offset(0, _resolvedPadding!.top); } size = constraints.constrain(Size( @@ -739,8 +735,8 @@ class RenderEditableTextLine extends RenderEditableBox { final parentData = _body!.parentData as BoxParentData; final effectiveOffset = offset + parentData.offset; if (enableInteractiveSelection && - line.getDocumentOffset() <= textSelection.end && - textSelection.start <= line.getDocumentOffset() + line.length - 1) { + line.documentOffset <= textSelection.end && + textSelection.start <= line.documentOffset + line.length - 1) { final local = localSelection(line, textSelection, false); _selectedRects ??= _body!.getBoxesForSelection( local, @@ -776,7 +772,7 @@ class RenderEditableTextLine extends RenderEditableBox { void _paintCursor(PaintingContext context, Offset effectiveOffset) { final position = TextPosition( - offset: textSelection.extentOffset - line.getDocumentOffset(), + offset: textSelection.extentOffset - line.documentOffset, affinity: textSelection.base.affinity, ); _cursorPainter.paint(context.canvas, effectiveOffset, position); @@ -851,8 +847,8 @@ class _TextLineElement extends RenderObjectElement { } void _mountChild(Widget? widget, TextLineSlot slot) { - Element? oldChild = _slotToChildren[slot]; - Element? newChild = updateChild(oldChild, widget, slot); + final oldChild = _slotToChildren[slot]; + final newChild = updateChild(oldChild, widget, slot); if (oldChild != null) { _slotToChildren.remove(slot); } @@ -875,8 +871,8 @@ class _TextLineElement extends RenderObjectElement { } void _updateChild(Widget? widget, TextLineSlot slot) { - Element? oldChild = _slotToChildren[slot]; - Element? newChild = updateChild(oldChild, widget, slot); + final oldChild = _slotToChildren[slot]; + final newChild = updateChild(oldChild, widget, slot); if (oldChild != null) { _slotToChildren.remove(slot); } diff --git a/lib/widgets/text_selection.dart b/lib/widgets/text_selection.dart index ff733de2..a8748de1 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -7,15 +7,15 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter_quill/models/documents/nodes/node.dart'; +import '../models/documents/nodes/node.dart'; import 'editor.dart'; TextSelection localSelection(Node node, TextSelection selection, fromParent) { - int base = fromParent ? node.getOffset() : node.getDocumentOffset(); + final base = fromParent ? node.offset : node.documentOffset; assert(base <= selection.end && selection.start <= base + node.length - 1); - int offset = fromParent ? node.getOffset() : node.getDocumentOffset(); + final offset = fromParent ? node.offset : node.documentOffset; return selection.copyWith( baseOffset: math.max(selection.start - offset, 0), extentOffset: math.min(selection.end - offset, node.length - 1)); @@ -24,6 +24,27 @@ TextSelection localSelection(Node node, TextSelection selection, fromParent) { enum _TextSelectionHandlePosition { START, END } class EditorTextSelectionOverlay { + EditorTextSelectionOverlay( + this.value, + this.handlesVisible, + this.context, + this.debugRequiredFor, + this.toolbarLayerLink, + this.startHandleLayerLink, + this.endHandleLayerLink, + this.renderObject, + this.selectionCtrls, + this.selectionDelegate, + this.dragStartBehavior, + this.onSelectionHandleTapped, + this.clipboardStatus, + ) { + final overlay = Overlay.of(context, rootOverlay: true)!; + + _toolbarController = AnimationController( + duration: const Duration(milliseconds: 150), vsync: overlay); + } + TextEditingValue value; bool handlesVisible = false; final BuildContext context; @@ -41,26 +62,6 @@ class EditorTextSelectionOverlay { List? _handles; OverlayEntry? toolbar; - EditorTextSelectionOverlay( - this.value, - this.handlesVisible, - this.context, - this.debugRequiredFor, - this.toolbarLayerLink, - this.startHandleLayerLink, - this.endHandleLayerLink, - this.renderObject, - this.selectionCtrls, - this.selectionDelegate, - this.dragStartBehavior, - this.onSelectionHandleTapped, - this.clipboardStatus) { - OverlayState overlay = Overlay.of(context, rootOverlay: true)!; - - _toolbarController = AnimationController( - duration: const Duration(milliseconds: 150), vsync: overlay); - } - TextSelection get _selection => value.selection; Animation get _toolbarOpacity => _toolbarController.view; @@ -99,7 +100,7 @@ class EditorTextSelectionOverlay { toolbar = OverlayEntry(builder: _buildToolbar); Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)! .insert(toolbar!); - _toolbarController.forward(from: 0.0); + _toolbarController.forward(from: 0); } Widget _buildHandle( @@ -111,7 +112,7 @@ class EditorTextSelectionOverlay { return Visibility( visible: handlesVisible, child: _TextSelectionHandleOverlay( - onSelectionHandleChanged: (TextSelection? newSelection) { + onSelectionHandleChanged: (newSelection) { _handleSelectionHandleChanged(newSelection, position); }, onSelectionHandleTapped: onSelectionHandleTapped, @@ -155,32 +156,32 @@ class EditorTextSelectionOverlay { default: throw 'Invalid position'; } - selectionDelegate.textEditingValue = - value.copyWith(selection: newSelection, composing: TextRange.empty); - selectionDelegate.bringIntoView(textPosition); + selectionDelegate + ..textEditingValue = + value.copyWith(selection: newSelection, composing: TextRange.empty) + ..bringIntoView(textPosition); } Widget _buildToolbar(BuildContext context) { - List endpoints = - renderObject!.getEndpointsForSelection(_selection); + final endpoints = renderObject!.getEndpointsForSelection(_selection); - Rect editingRegion = Rect.fromPoints( + final editingRegion = Rect.fromPoints( renderObject!.localToGlobal(Offset.zero), renderObject!.localToGlobal(renderObject!.size.bottomRight(Offset.zero)), ); - double baseLineHeight = renderObject!.preferredLineHeight(_selection.base); - double extentLineHeight = + final baseLineHeight = renderObject!.preferredLineHeight(_selection.base); + final extentLineHeight = renderObject!.preferredLineHeight(_selection.extent); - double smallestLineHeight = math.min(baseLineHeight, extentLineHeight); - bool isMultiline = endpoints.last.point.dy - endpoints.first.point.dy > + final smallestLineHeight = math.min(baseLineHeight, extentLineHeight); + final isMultiline = endpoints.last.point.dy - endpoints.first.point.dy > smallestLineHeight / 2; - double midX = isMultiline + final midX = isMultiline ? editingRegion.width / 2 : (endpoints.first.point.dx + endpoints.last.point.dx) / 2; - Offset midpoint = Offset( + final midpoint = Offset( midX, endpoints[0].point.dy - baseLineHeight, ); @@ -232,10 +233,10 @@ class EditorTextSelectionOverlay { assert(_handles == null); _handles = [ OverlayEntry( - builder: (BuildContext context) => + builder: (context) => _buildHandle(context, _TextSelectionHandlePosition.START)), OverlayEntry( - builder: (BuildContext context) => + builder: (context) => _buildHandle(context, _TextSelectionHandlePosition.END)), ]; @@ -326,14 +327,14 @@ class _TextSelectionHandleOverlayState void _handleDragStart(DragStartDetails details) {} void _handleDragUpdate(DragUpdateDetails details) { - TextPosition position = + final position = widget.renderObject!.getPositionForOffset(details.globalPosition); if (widget.selection.isCollapsed) { widget.onSelectionHandleChanged(TextSelection.fromPosition(position)); return; } - bool isNormalized = + final isNormalized = widget.selection.extentOffset >= widget.selection.baseOffset; TextSelection? newSelection; switch (widget.position) { @@ -389,27 +390,26 @@ class _TextSelectionHandleOverlayState break; } - TextPosition textPosition = - widget.position == _TextSelectionHandlePosition.START - ? widget.selection.base - : widget.selection.extent; - double lineHeight = widget.renderObject!.preferredLineHeight(textPosition); - Offset handleAnchor = + final textPosition = widget.position == _TextSelectionHandlePosition.START + ? widget.selection.base + : widget.selection.extent; + final lineHeight = widget.renderObject!.preferredLineHeight(textPosition); + final handleAnchor = widget.selectionControls.getHandleAnchor(type!, lineHeight); - Size handleSize = widget.selectionControls.getHandleSize(lineHeight); + final handleSize = widget.selectionControls.getHandleSize(lineHeight); - Rect handleRect = Rect.fromLTWH( + final handleRect = Rect.fromLTWH( -handleAnchor.dx, -handleAnchor.dy, handleSize.width, handleSize.height, ); - Rect interactiveRect = handleRect.expandToInclude( + final interactiveRect = handleRect.expandToInclude( Rect.fromCircle( center: handleRect.center, radius: kMinInteractiveDimension / 2), ); - RelativeRect padding = RelativeRect.fromLTRB( + final padding = RelativeRect.fromLTRB( math.max((interactiveRect.width - handleRect.width) / 2, 0), math.max((interactiveRect.height - handleRect.height) / 2, 0), math.max((interactiveRect.width - handleRect.width) / 2, 0), @@ -657,13 +657,12 @@ class _EditorTextSelectionGestureDetectorState @override Widget build(BuildContext context) { - final Map gestures = - {}; + final gestures = {}; gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => TapGestureRecognizer(debugOwner: this), - (TapGestureRecognizer instance) { + (instance) { instance ..onTapDown = _handleTapDown ..onTapUp = _handleTapUp @@ -678,7 +677,7 @@ class _EditorTextSelectionGestureDetectorState GestureRecognizerFactoryWithHandlers( () => LongPressGestureRecognizer( debugOwner: this, kind: PointerDeviceKind.touch), - (LongPressGestureRecognizer instance) { + (instance) { instance ..onLongPressStart = _handleLongPressStart ..onLongPressMoveUpdate = _handleLongPressMoveUpdate @@ -694,7 +693,7 @@ class _EditorTextSelectionGestureDetectorState GestureRecognizerFactoryWithHandlers( () => HorizontalDragGestureRecognizer( debugOwner: this, kind: PointerDeviceKind.mouse), - (HorizontalDragGestureRecognizer instance) { + (instance) { instance ..dragStartBehavior = DragStartBehavior.down ..onStart = _handleDragStart @@ -708,7 +707,7 @@ class _EditorTextSelectionGestureDetectorState gestures[ForcePressGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => ForcePressGestureRecognizer(debugOwner: this), - (ForcePressGestureRecognizer instance) { + (instance) { instance ..onStart = widget.onForcePressStart != null ? _forcePressStarted : null diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 6b4b4147..71ddec1d 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -5,31 +5,31 @@ import 'package:filesystem_picker/filesystem_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; -import 'package:flutter_quill/models/documents/attribute.dart'; -import 'package:flutter_quill/models/documents/nodes/embed.dart'; -import 'package:flutter_quill/models/documents/style.dart'; -import 'package:flutter_quill/utils/color.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; +import '../models/documents/attribute.dart'; +import '../models/documents/nodes/embed.dart'; +import '../models/documents/style.dart'; +import '../utils/color.dart'; import 'controller.dart'; -double iconSize = 18.0; +double iconSize = 18; double kToolbarHeight = iconSize * 2; typedef OnImagePickCallback = Future Function(File file); typedef ImagePickImpl = Future Function(ImageSource source); class InsertEmbedButton extends StatelessWidget { - final QuillController controller; - final IconData icon; - const InsertEmbedButton({ required this.controller, required this.icon, Key? key, }) : super(key: key); + final QuillController controller; + final IconData icon; + @override Widget build(BuildContext context) { return QuillIconButton( @@ -52,15 +52,15 @@ class InsertEmbedButton extends StatelessWidget { } class LinkStyleButton extends StatefulWidget { - final QuillController controller; - final IconData? icon; - const LinkStyleButton({ required this.controller, this.icon, Key? key, }) : super(key: key); + final QuillController controller; + final IconData? icon; + @override _LinkStyleButtonState createState() => _LinkStyleButtonState(); } @@ -174,14 +174,6 @@ typedef ToggleStyleButtonBuilder = Widget Function( ); class ToggleStyleButton extends StatefulWidget { - final Attribute attribute; - - final IconData icon; - - final QuillController controller; - - final ToggleStyleButtonBuilder childBuilder; - const ToggleStyleButton({ required this.attribute, required this.icon, @@ -190,6 +182,14 @@ class ToggleStyleButton extends StatefulWidget { Key? key, }) : super(key: key); + final Attribute attribute; + + final IconData icon; + + final QuillController controller; + + final ToggleStyleButtonBuilder childBuilder; + @override _ToggleStyleButtonState createState() => _ToggleStyleButtonState(); } @@ -215,7 +215,7 @@ class _ToggleStyleButtonState extends State { bool _getIsToggled(Map attrs) { if (widget.attribute.key == Attribute.list.key) { - Attribute? attribute = attrs[widget.attribute.key]; + final attribute = attrs[widget.attribute.key]; if (attribute == null) { return false; } @@ -258,14 +258,6 @@ class _ToggleStyleButtonState extends State { } class ToggleCheckListButton extends StatefulWidget { - final IconData icon; - - final QuillController controller; - - final ToggleStyleButtonBuilder childBuilder; - - final Attribute attribute; - const ToggleCheckListButton({ required this.icon, required this.controller, @@ -274,6 +266,14 @@ class ToggleCheckListButton extends StatefulWidget { Key? key, }) : super(key: key); + final IconData icon; + + final QuillController controller; + + final ToggleStyleButtonBuilder childBuilder; + + final Attribute attribute; + @override _ToggleCheckListButtonState createState() => _ToggleCheckListButtonState(); } @@ -299,7 +299,7 @@ class _ToggleCheckListButtonState extends State { bool _getIsToggled(Map attrs) { if (widget.attribute.key == Attribute.list.key) { - Attribute? attribute = attrs[widget.attribute.key]; + final attribute = attrs[widget.attribute.key]; if (attribute == null) { return false; } @@ -369,11 +369,11 @@ Widget defaultToggleStyleButtonBuilder( } class SelectHeaderStyleButton extends StatefulWidget { - final QuillController controller; - const SelectHeaderStyleButton({required this.controller, Key? key}) : super(key: key); + final QuillController controller; + @override _SelectHeaderStyleButtonState createState() => _SelectHeaderStyleButtonState(); @@ -430,20 +430,20 @@ class _SelectHeaderStyleButtonState extends State { Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value, ValueChanged onSelected) { - final Map _valueToText = { + final _valueToText = { Attribute.header: 'N', Attribute.h1: 'H1', Attribute.h2: 'H2', Attribute.h3: 'H3', }; - List _valueAttribute = [ + final _valueAttribute = [ Attribute.header, Attribute.h1, Attribute.h2, Attribute.h3 ]; - List _valueString = ['N', 'H1', 'H2', 'H3']; + final _valueString = ['N', 'H1', 'H2', 'H3']; final theme = Theme.of(context); final style = TextStyle( @@ -464,7 +464,7 @@ Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value, child: RawMaterialButton( hoverElevation: 0, highlightElevation: 0, - elevation: 0.0, + elevation: 0, visualDensity: VisualDensity.compact, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), @@ -490,6 +490,15 @@ Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value, } class ImageButton extends StatefulWidget { + const ImageButton({ + required this.icon, + required this.controller, + required this.imageSource, + this.onImagePickCallback, + this.imagePickImpl, + Key? key, + }) : super(key: key); + final IconData icon; final QuillController controller; @@ -500,15 +509,6 @@ class ImageButton extends StatefulWidget { final ImageSource imageSource; - const ImageButton({ - required this.icon, - required this.controller, - required this.imageSource, - this.onImagePickCallback, - this.imagePickImpl, - Key? key, - }) : super(key: key); - @override _ImageButtonState createState() => _ImageButtonState(); } @@ -520,10 +520,10 @@ class _ImageButtonState extends State { final FileType _pickingType = FileType.any; Future _pickImage(ImageSource source) async { - final PickedFile? pickedFile = await _picker.getImage(source: source); + final pickedFile = await _picker.getImage(source: source); if (pickedFile == null) return null; - final File file = File(pickedFile.path); + final file = File(pickedFile.path); return widget.onImagePickCallback!(file); } @@ -536,11 +536,11 @@ class _ImageButtonState extends State { : null, )) ?.files; - var _fileName = + final _fileName = _paths != null ? _paths!.map((e) => e.name).toString() : '...'; if (_paths != null) { - File file = File(_fileName); + final file = File(_fileName); // We simply return the absolute path to selected file. return widget.onImagePickCallback!(file); } else { @@ -550,7 +550,7 @@ class _ImageButtonState extends State { } Future _pickImageDesktop() async { - var filePath = await FilesystemPicker.open( + final filePath = await FilesystemPicker.open( context: context, rootDirectory: await getApplicationDocumentsDirectory(), fsType: FilesystemType.file, @@ -558,7 +558,7 @@ class _ImageButtonState extends State { ); if (filePath != null && filePath.isEmpty) return ''; - final File file = File(filePath!); + final file = File(filePath!); return widget.onImagePickCallback!(file); } @@ -602,10 +602,6 @@ class _ImageButtonState extends State { /// When pressed, this button displays overlay toolbar with /// buttons for each color. class ColorButton extends StatefulWidget { - final IconData icon; - final bool background; - final QuillController controller; - const ColorButton({ required this.icon, required this.controller, @@ -613,6 +609,10 @@ class ColorButton extends StatefulWidget { Key? key, }) : super(key: key); + final IconData icon; + final bool background; + final QuillController controller; + @override _ColorButtonState createState() => _ColorButtonState(); } @@ -683,19 +683,19 @@ class _ColorButtonState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - Color? iconColor = _isToggledColor && !widget.background && !_isWhite + final iconColor = _isToggledColor && !widget.background && !_isWhite ? stringToColor(_selectionStyle.attributes['color']!.value) : theme.iconTheme.color; - var iconColorBackground = + final iconColorBackground = _isToggledBackground && widget.background && !_isWhitebackground ? stringToColor(_selectionStyle.attributes['background']!.value) : theme.iconTheme.color; - Color fillColor = _isToggledColor && !widget.background && _isWhite + final fillColor = _isToggledColor && !widget.background && _isWhite ? stringToColor('#ffffff') : theme.canvasColor; - Color fillColorBackground = + final fillColorBackground = _isToggledBackground && widget.background && _isWhitebackground ? stringToColor('#ffffff') : theme.canvasColor; @@ -713,7 +713,7 @@ class _ColorButtonState extends State { } void _changeColor(Color color) { - String hex = color.value.toRadixString(16); + var hex = color.value.toRadixString(16); if (hex.startsWith('ff')) { hex = hex.substring(2); } @@ -740,10 +740,6 @@ class _ColorButtonState extends State { } class HistoryButton extends StatefulWidget { - final IconData icon; - final bool undo; - final QuillController controller; - const HistoryButton({ required this.icon, required this.controller, @@ -751,6 +747,10 @@ class HistoryButton extends StatefulWidget { Key? key, }) : super(key: key); + final IconData icon; + final bool undo; + final QuillController controller; + @override _HistoryButtonState createState() => _HistoryButtonState(); } @@ -812,10 +812,6 @@ class _HistoryButtonState extends State { } class IndentButton extends StatefulWidget { - final IconData icon; - final QuillController controller; - final bool isIncrease; - const IndentButton({ required this.icon, required this.controller, @@ -823,6 +819,10 @@ class IndentButton extends StatefulWidget { Key? key, }) : super(key: key); + final IconData icon; + final QuillController controller; + final bool isIncrease; + @override _IndentButtonState createState() => _IndentButtonState(); } @@ -867,16 +867,16 @@ class _IndentButtonState extends State { } class ClearFormatButton extends StatefulWidget { - final IconData icon; - - final QuillController controller; - const ClearFormatButton({ required this.icon, required this.controller, Key? key, }) : super(key: key); + final IconData icon; + + final QuillController controller; + @override _ClearFormatButtonState createState() => _ClearFormatButtonState(); } @@ -894,7 +894,7 @@ class _ClearFormatButtonState extends State { icon: Icon(widget.icon, size: iconSize, color: iconColor), fillColor: fillColor, onPressed: () { - for (Attribute k + for (final k in widget.controller.getSelectionStyle().attributes.values) { widget.controller.formatSelection(Attribute.clone(k, null)); } @@ -903,8 +903,6 @@ class _ClearFormatButtonState extends State { } class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { - final List children; - const QuillToolbar({required this.children, Key? key}) : super(key: key); factory QuillToolbar.basic({ @@ -1117,6 +1115,8 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { ]); } + final List children; + @override _QuillToolbarState createState() => _QuillToolbarState(); @@ -1148,13 +1148,6 @@ class _QuillToolbarState extends State { } class QuillIconButton extends StatelessWidget { - final VoidCallback? onPressed; - final Widget? icon; - final double size; - final Color? fillColor; - final double hoverElevation; - final double highlightElevation; - const QuillIconButton({ required this.onPressed, this.icon, @@ -1165,6 +1158,13 @@ class QuillIconButton extends StatelessWidget { Key? key, }) : super(key: key); + final VoidCallback? onPressed; + final Widget? icon; + final double size; + final Color? fillColor; + final double hoverElevation; + final double highlightElevation; + @override Widget build(BuildContext context) { return ConstrainedBox( @@ -1184,15 +1184,6 @@ class QuillIconButton extends StatelessWidget { } class QuillDropdownButton extends StatefulWidget { - final double height; - final Color? fillColor; - final double hoverElevation; - final double highlightElevation; - final Widget child; - final T initialValue; - final List> items; - final ValueChanged onSelected; - const QuillDropdownButton({ required this.child, required this.initialValue, @@ -1205,6 +1196,15 @@ class QuillDropdownButton extends StatefulWidget { Key? key, }) : super(key: key); + final double height; + final Color? fillColor; + final double hoverElevation; + final double highlightElevation; + final Widget child; + final T initialValue; + final List> items; + final ValueChanged onSelected; + @override _QuillDropdownButtonState createState() => _QuillDropdownButtonState(); } @@ -1251,7 +1251,7 @@ class _QuillDropdownButtonState extends State> { // widget.shape ?? popupMenuTheme.shape, color: popupMenuTheme.color, // widget.color ?? popupMenuTheme.color, // captureInheritedThemes: widget.captureInheritedThemes, - ).then((T? newValue) { + ).then((newValue) { if (!mounted) return null; if (newValue == null) { // if (widget.onCanceled != null) widget.onCanceled(); @@ -1265,7 +1265,7 @@ class _QuillDropdownButtonState extends State> { return ConstrainedBox( constraints: const BoxConstraints.tightFor(width: 110), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), + padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( children: [ widget.child, diff --git a/pubspec.yaml b/pubspec.yaml index 504f96d1..15c4de29 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.1.7 +version: 1.1.8 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill